确保你已经阅读了《PHP7扩展开发教程[7] – 如何创建对象?》。
本章节的知识点比较少,但是会更多深入到zend代码里去。
内容涉及:
- 注册和获取全局常量。
- 读取全局超级变量,例如:$_GET,$_POST,$_SERVER。
正式开始
本章代码:https://github.com/owenliang/php7-extension-explore/tree/master/course8-how-to-visit-global-vars。
在扩展的启动回调中:
1 |
int extension_startup(int type, int module_number) |
我们注册一个全局常量:
1 2 3 4 5 6 7 8 |
// register constant zend_constant c; c.name = zend_string_init("GITHUB", sizeof("GITHUB") - 1, 1); // persistant memory ZVAL_STR(&c.value, zend_string_init("https://github.com/owenliang/php7-extension-explore", sizeof("https://github.com/owenliang/php7-extension-explore"), 1)); // persistant memory c.flags = CONST_CS | CONST_PERSISTENT; c.module_number = module_number; assert(zend_register_constant(&c) == SUCCESS); |
首先定义一个zend_constant结构,它的定义如下:
1 2 3 4 5 6 |
typedef struct _zend_constant { zval value; zend_string *name; int flags; int module_number; } zend_constant; |
注意它不是像zend_string一样存储在zval的底层数据结构,而是一个独立的类型。因为常量在整个PHP执行期间始终存在,所以填充zend_constant的各个字段都使用了持久化内存分配。
name是常量的名称,叫做GITHUB。value是一个zval类型,代表常量的值,这里我分配了一个string类型的zval。flags是常量的属性,它的枚举值包含:
1 2 3 |
#define CONST_CS (1<<0) /* Case Sensitive */ #define CONST_PERSISTENT (1<<1) /* Persistent */ #define CONST_CT_SUBST (1<<2) /* Allow compile-time substitution */ |
一般都是使用前两个,CONST_CS代表常量的名字是大小写敏感的,CONST_PERSISTENT表示这个常量持久存在。最后一个CONST_CT_SUBST没有见到使用的,从注释来看像是允许用户PHP代码里覆盖这个常量的值。
最后的module_number是回调函数extension_startup传给我们的模块唯一标识,从而说明这个常量是我们的扩展注册的。
接下来通过zend_register_constant方法注册这个常量,其定义如下:
1 |
ZEND_API int zend_register_constant(zend_constant *c) |
这样注册之后,在PHP代码里就可以像这样访问它了:
1 |
echo GITHUB . PHP_EOL; |
那么,在扩展里又该如何访问到这个常量呢?在myext类的成员方法中:
1 |
void zim_myext_version(zend_execute_data *execute_data, zval *return_value) |
我在末尾添加了一段读取常量的代码:
1 2 3 4 5 6 |
// read constant zend_string *cname = zend_string_init("GITHUB", sizeof("GITHUB") - 1, 0); zval *c_github = zend_get_constant(cname); assert(Z_TYPE_P(c_github) == IS_STRING); TRACE("zend_get_constant(GITHUB)=%.*s", Z_STRLEN_P(c_github), Z_STRVAL_P(c_github)); zend_string_release(cname); |
首先创建一个zend_string表示常量的名字,然后通过zend_get_constant即可获得其zval,这个zval就是之前注册时zend_constant对象里的那个zval。通过断言确认它是字符串类型,打印其值的确是之前注册的内容,最后不要忘记释放cname的内存。
zend_get_constant的实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
ZEND_API zval *zend_get_constant(zend_string *name) { zend_constant *c; ALLOCA_FLAG(use_heap) if ((c = zend_hash_find_ptr(EG(zend_constants), name)) == NULL) { char *lcname = do_alloca(ZSTR_LEN(name) + 1, use_heap); zend_str_tolower_copy(lcname, ZSTR_VAL(name), ZSTR_LEN(name)); if ((c = zend_hash_str_find_ptr(EG(zend_constants), lcname, ZSTR_LEN(name))) != NULL) { if (c->flags & CONST_CS) { c = NULL; } } else { c = zend_get_special_constant(ZSTR_VAL(name), ZSTR_LEN(name)); } free_alloca(lcname, use_heap); } return c ? &c->value : NULL; } |
你会发现,常量最终保存在一个全局的哈希表EG(zend_constants)里。EG这个宏是指executor globals,大概就是指执行时的全局变量,它最终指向一个全局单例对象zend_executor_globals zend_executor_globals,其具体定义如下:
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 |
# define EG(v) (executor_globals.v) extern ZEND_API zend_executor_globals executor_globals; typedef struct _zend_executor_globals zend_executor_globals; struct _zend_executor_globals { zval uninitialized_zval; zval error_zval; /* symbol table cache */ zend_array *symtable_cache[SYMTABLE_CACHE_SIZE]; zend_array **symtable_cache_limit; zend_array **symtable_cache_ptr; zend_array symbol_table; /* main symbol table */ HashTable included_files; /* files already included */ JMP_BUF *bailout; int error_reporting; int exit_status; HashTable *function_table; /* function symbol table */ HashTable *class_table; /* class table */ HashTable *zend_constants; /* constants table */ zval *vm_stack_top; zval *vm_stack_end; zend_vm_stack vm_stack; struct _zend_execute_data *current_execute_data; |
可见除了zend_constants,我们的全局函数应该是保存在function_table中,class应该保存在class_table中,而我们的zif/zim函数的第一个回调参数zend_execute_data其实就是EG里的current_execute_data,大概了解这个关系即可。
另外一个常见的需求,就是获取全局超级变量,例如:$_SERVER,$_GET…。在一个扩展中,又该如何获取呢?我将相关的代码实现在如下函数中:
1 |
int extension_before_request(int type, int module_number) |
这里我假设要访问$_SERVER,首先调用了:
1 2 |
// try active jit super globals zend_is_auto_global_str("_SERVER", sizeof("_SERVER") - 1); |
它会帮我们确保_SERVER已经被注册到全局符号表,这是什么意思呢?不如从Zend的源码里找出来龙去脉,以便用的明明白白。
1 2 3 4 5 6 7 8 9 10 |
void php_startup_auto_globals(void) { zend_register_auto_global(zend_string_init("_GET", sizeof("_GET")-1, 1), 0, php_auto_globals_create_get); zend_register_auto_global(zend_string_init("_POST", sizeof("_POST")-1, 1), 0, php_auto_globals_create_post); zend_register_auto_global(zend_string_init("_COOKIE", sizeof("_COOKIE")-1, 1), 0, php_auto_globals_create_cookie); zend_register_auto_global(zend_string_init("_SERVER", sizeof("_SERVER")-1, 1), PG(auto_globals_jit), php_auto_globals_create_server); zend_register_auto_global(zend_string_init("_ENV", sizeof("_ENV")-1, 1), PG(auto_globals_jit), php_auto_globals_create_env); zend_register_auto_global(zend_string_init("_REQUEST", sizeof("_REQUEST")-1, 1), PG(auto_globals_jit), php_auto_globals_create_request); zend_register_auto_global(zend_string_init("_FILES", sizeof("_FILES")-1, 1), 0, php_auto_globals_create_files); } |
PHP在处理一个请求之前,会执行这个函数来注册若干的超级全局变量,比如_GET,_POST。那么zend_register_auto_global做了什么呢?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
int zend_register_auto_global(zend_string *name, zend_bool jit, zend_auto_global_callback auto_global_callback) /* {{{ */ { zend_auto_global auto_global; int retval; auto_global.name = zend_new_interned_string(name); auto_global.auto_global_callback = auto_global_callback; auto_global.jit = jit; retval = zend_hash_add_mem(CG(auto_globals), auto_global.name, &auto_global, sizeof(zend_auto_global)) != NULL ? SUCCESS : FAILURE; zend_string_release(name); return retval; } |
它将一个zend_auto_global对象保存了CG(auto_globals)这个全局变量里,CG类似于EG,稍后再看一下其定义即可。
先来看一下zend_auto_global的定义:
1 2 3 4 5 6 7 |
typedef zend_bool (*zend_auto_global_callback)(zend_string *name); typedef struct _zend_auto_global { zend_string *name; zend_auto_global_callback auto_global_callback; zend_bool jit; zend_bool armed; } zend_auto_global; |
name是超级全局变量的名字,auto_global_callback是一个回调函数,未来将用于初始化超级变量的值。jit是JUST IN TIME的意思,即时编译,在调用zend_register_auto_global的时候,有的传了0有的传了1,具体用途稍后再说。armed字段表示该超级全局变量是否完成初始化,但是这里尚且没有赋值,稍后解释。
注册了若干超级全局变量之后,PHP其实紧接着调用了这个函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
ZEND_API void zend_activate_auto_globals(void) /* {{{ */ { zend_auto_global *auto_global; ZEND_HASH_FOREACH_PTR(CG(auto_globals), auto_global) { if (auto_global->jit) { auto_global->armed = 1; } else if (auto_global->auto_global_callback) { auto_global->armed = auto_global->auto_global_callback(auto_global->name); } else { auto_global->armed = 0; } } ZEND_HASH_FOREACH_END(); } |
之前register方法将所有超级全局变量注册到了CG(auto_globals)这个哈希表里,其含义是compiler globals,结构体片段如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
# define CG(v) (compiler_globals.v) extern ZEND_API struct _zend_compiler_globals compiler_globals; struct _zend_compiler_globals { zend_stack loop_var_stack; zend_class_entry *active_class_entry; zend_string *compiled_filename; int zend_lineno; zend_op_array *active_op_array; HashTable *function_table; /* function symbol table */ HashTable *class_table; /* class table */ HashTable filenames_table; HashTable *auto_globals; |
CG是编译期全局变量,大概是在解释PHP代码为opcode期间保存信息的空间。EG是执行期全局变量,大概就是运行期间保存信息的空间。一个PHP代码要经历编译 -> 解释执行的过程,所以这是我的大概理解。
回到函数:
1 |
ZEND_API void zend_activate_auto_globals(void) /* {{{ */ |
它遍历了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):
1 2 3 4 5 6 7 8 9 10 11 12 |
zend_bool zend_is_auto_global(zend_string *name) /* {{{ */ { zend_auto_global *auto_global; if ((auto_global = zend_hash_find_ptr(CG(auto_globals), name)) != NULL) { if (auto_global->armed) { auto_global->armed = auto_global->auto_global_callback(auto_global->name); } return 1; } return 0; } |
它去CG(auto_globals)找到这个超级全局变量,如果armed为1表示尚未初始化,所以就调用它的初始化回调auto_global_callback初始化超级全局变量的值,初始化成功与否赋值给armed,下次就不会重复的初始化了。
那么auto_global_callback一般做了什么呢?以_GET的初始化回调函数为例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
static zend_bool php_auto_globals_create_get(zend_string *name) { if (PG(variables_order) && (strchr(PG(variables_order),'G') || strchr(PG(variables_order),'g'))) { sapi_module.treat_data(PARSE_GET, NULL, NULL); } else { zval_ptr_dtor(&PG(http_globals)[TRACK_VARS_GET]); array_init(&PG(http_globals)[TRACK_VARS_GET]); } zend_hash_update(&EG(symbol_table), name, &PG(http_globals)[TRACK_VARS_GET]); Z_ADDREF(PG(http_globals)[TRACK_VARS_GET]); return 0; /* don't rearm */ } |
从代码角度来看,_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变量了:
1 2 3 4 |
// find it in global symbol table zval *server = zend_hash_str_find(&EG(symbol_table), "_SERVER", sizeof("_SERVER") - 1); assert(server != NULL); assert(Z_TYPE_P(server) == IS_ARRAY); |
根据我们的PHP开发经验,$_GET是一个数组类型。
接着,我利用PHP函数var_dump,将这个数组打印出来:
1 2 3 4 5 6 7 8 |
// var_dump($_SERVER) zval func_name; ZVAL_STR(&func_name, zend_string_init("var_dump", sizeof("var_dump") - 1, 0)); zval retval; assert(call_user_function(&EG(function_table), NULL, &func_name, &retval, 1, server) == SUCCESS); zval_ptr_dtor(&func_name); zval_ptr_dtor(&retval); return SUCCESS; |
调用PHP函数的方式我们再熟悉不过,所以就不再详细说明了。
结语
本章你应该掌握:
- 全局常量的注册和访问。
- 超级全局变量的访问。
- CG,EG,PG大概的样子和关系。
在下一章中,我将特别的分析zend_array数组类型的API用法,以及简单介绍其数据结构实现原理。
如果文章帮助您解决了工作难题,您可以帮我点击屏幕上的任意广告,或者赞助少量费用来支持我的持续创作,谢谢~

Pingback引用通告: PHP7扩展开发教程[9] – 如何使用哈希表? | 鱼儿的博客
niubility