很長時間沒有更新博客了. 一來最近工做比較忙,沒有時間好好研究問題, 二是以爲沒有很好的材料能夠寫. 也有一些沒有完全研究透的問題,寫着寫着沒有了頭緒,都扔在了草稿箱裏了. 此次順帶也要更新一下博客的模版了, 如今的這個模版主體有點窄,不適合閱讀. 我這個博客如今,之後主要仍是寫一些技術的東西.仍是換一個眼睛友好的主題吧.php
本文要解決的是從去年就一直在考慮的一個PHP的問題: 怎麼樣獲取PHP變量的變量名. 一直以來都沒有好好的研究.最近斷斷續續的開始看PHP源代碼.並嘗試解決. 直到兩星期前把問題都解決了纔開始把這些東西都記下來.node
若是有興趣先看看這個功能是怎麼實現的. 能夠先 點擊這裏下載代碼 .linux
一年多前作一個模版引擎的何時有了這樣一個需求: 獲取變量的變量名. 好比:json
1vim 2數組 3函數 4工具 5學習 |
$some_variable_name = "blahblah";ui //... echo get_var_name($some_variable_name); // 這裏指望輸出"some_variable_name"; ?> |
若是你也有這樣的需求. 你對需求的理解絕對有問題. 不事後來想一想這需求雖然不合理. 可是若是我偏有這樣不合理的需求, 我有辦法真的能知足麼?
在遇到這個問題以前,沒有太系統的去看過PHP的C實現. 從問題提出到目前爲止,我想到了以下幾種方法:
直接寫一個PHP函數來獲取.好比:
1 2 3 4 |
function get_var_name($var) { // 可是... 我怎麼的到變量的名字呢... // echo ? How To? } |
用過$GLOBALS變量的人應該知道能夠經過 $GLOBALS[\'var\']的方式來獲取變量$var的值. 這樣的話,我應該就能這樣實現了
1 2 3 4 5 6 7 |
function get_var_name($var) { foreach($GLOBALS as $var_name => $var_value) { if($var === $var_value) { return $var_name; } } } |
這個是不可行的. 首先, 這個方法只能返回全局做用域內的變量. 若是在函數體內調用這個函數會有問題. 而且經過值比較也徹底不可靠.
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 |
struct _zend_executor_globals { zval **return_value_ptr_ptr; zval uninitialized_zval; zval *uninitialized_zval_ptr; zval error_zval; zval *error_zval_ptr; zend_ptr_stack arg_types_stack; /* symbol table cache */ HashTable *symtable_cache[SYMTABLE_CACHE_SIZE]; HashTable **symtable_cache_limit; HashTable **symtable_cache_ptr; zend_op **opline_ptr; HashTable *active_symbol_table; // 當前做用域的變量符號表 HashTable symbol_table; /* main symbol table */ // 全局符號表 HashTable included_files; /* files already included */ //.. }; |
好比模塊提供一個叫作get_var_name()的函數來獲取變量名字. 若是你們有寫過PHP擴展的經驗的話,應該看過相似以下的函數實現(取自php json擴展$PHP_SRC/ext/json/json.c):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
/* {{{ proto string json_encode(mixed data [, int options]) Returns the JSON representation of a value */ static PHP_FUNCTION(json_encode) { zval *parameter; smart_str buf = {0}; long options = 0; if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "z|l", ¶meter, &options) == FAILURE) { // 這裏將傳入的參數取出來. 參考文檔 http://www.php.net/manual/en/internals2.funcs.php return; } php_json_encode(&buf, parameter, options TSRMLS_CC); ZVAL_STRINGL(return_value, buf.c, buf.len, 1); smart_str_free(&buf); } /* }}} */ |
這中實如今函數體內能夠經過zend_parse_parameter的方式來獲取傳遞進來的變量, 但這樣只能獲取到變量的值. 卻沒法獲得其餘更多的信息.,咱們往低層看看在PHP中函數是怎麼調用,參數是怎麼傳遞的.
在研究函數怎麼調用以前, 咱們須要看看PHP代碼是怎麼執行的.
大體能夠分爲2個步驟:
- 詞法分析,語法分析而後編譯成opcode
- 執行opcode
PHP函數的執行也只能在opcode執行階段執行.
這裏以前要介紹一個查看OPCODE的絕佳工具 vld(http://pecl.php.net/package/vld)
裝好這擴展。能夠在命令行下查看php腳本編譯後的opcode
咱們看看下面這個php腳本被編譯後opcode是什麼樣的.
1 2 3 4 5 6 7 8 9 10 |
也能夠再增長一個參數 -dvld.verbosity=3, 這樣將會顯示更多的信息.
它被編譯爲上面的10條opcode命令.
op的名稱一看也能看出什麼意思. .. 其中以 「!」開頭的數字表示編譯後的變量,, 以」~」開頭的變量表示零時變量.
上面可能夠看出若是函數調用存在參數的話,在DO_FCALL以前會執行SEND_VAR 或者 SEND_VAR_NO_REF指令。而且這些指令後面操做的是編譯過變量或者一個臨時變量.
在PHP中調用時咱們是能夠訪問到DO_FCALL這個操做的opcode信息 。能夠經過 EG(active_opline_ptr) 獲取到當前指令
PHP中存在一系列*G宏, EG 則爲在執行opcode時的全局變量。
見文件: $PHP_SRC/Zend/zend_globals.h
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 35 36 37 38 39 40 |
struct _zend_executor_globals { // ... zend_op **opline_ptr; // 指向當前正在執行的zend_op對象 HashTable *active_symbol_table; HashTable symbol_table; /* main symbol table */ HashTable included_files; /* files already included */ jmp_buf *bailout; int error_reporting; int orig_error_reporting; int exit_status; zend_op_array *active_op_array; // ... }; struct _zend_op_array { /* Common elements */ zend_uchar type; char *function_name; zend_class_entry *scope; zend_uint fn_flags; union _zend_function *prototype; zend_uint num_args; zend_uint required_num_args; zend_arg_info *arg_info; zend_bool pass_rest_by_reference; unsigned char return_reference; /* END of common elements */ zend_bool done_pass_two; zend_uint *refcount; zend_op *opcodes; // zend_op數組. zend_uint last, size; zend_compiled_variable *vars; // 全部編譯後的變量信息Since PHP5.1 這是一個數組 int last_var, size_var; // last_var 最後一個編譯變量的索引 // ... }; |
當前執行的op_array中保存全部編譯變量的信息, 再看看zend_compiled_variable的結構吧。
1 2 3 4 5 |
typedef struct _zend_compiled_variable { char *name; int name_len; ulong hash_value; } zend_compiled_variable; |
這正是我想獲取的變量名稱.
咱們能夠經過全局變量EG(opline_ptr)指針獲取到當前執行的zend_op, zend_op的結構以下:
1 2 3 4 5 6 7 8 9 |
struct _zend_op { opcode_handler_t handler; // 處理該OPCODE的處理函數 znode result; // 該opcode執行的結果 znode op1; // 有的opcode須要1個,有的須要兩個操做數。 znode op2; ulong extended_value; uint lineno; zend_uchar opcode; // 該opcode的值 見$PHP_SRC/Zend/zend_vm_opcodes.h }; |
這也就是咱們函數調用時執行的opcode.咱們如今能夠獲取到DO_FCALL時的opcode, 經過VLD察看opcode工具很容易就知道函數調用以前,若是函數有參數的話,在DO_FCALL以前必定有SEND_VAR或者 SEND_VAR_NO_REF指令, 指針後退一個則必定是指向SEND_VAR或SEND_VAR_NO_REF指令的。 這樣的話咱們根據DO_FCALL獲取到的zend_op指令後退不久能夠獲取SEND_VAR指令了麼. SEND_VAR指令會操做compiled_var,這樣咱們就能獲得變量的信息了..
看看znode都有哪些信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
typedef struct _znode { int op_type; union { zval constant ; zend_uint var; // 這個var就是當前變量在zend_op_array.vars 中的compiled_variable數組中的索引.不過這個索要並非字面上的. 詳情請看最後的代碼實現. zend_uint opline_num; /* Needs to be signed */ zend_op_array *op_array; zend_op *jmp_addr; struct { zend_uint var; /* dummy */ zend_uint type; } EA; } u; } znode; |
如在在上面的註釋. 經過獲取znode.u.var的值就能夠獲取到變量的信息了.
這樣的話.程序的實現也就簡單了.
下面是實現:
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 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
/* {{{ get_var_name * * 這個擴展要求PHP >= 5.1 * 由於依賴PHP 5.1引入的compiled variable * * 在PHP空間導出一個get_var_name函數. * echo get_var_name($var_name); // expect: var_name * echo get_var_name($lineno=100); // expect: lineno */ PHP_FUNCTION(get_var_name) { int len; char *strg = ""; if(ZEND_NUM_ARGS() < 1) { return; } /* 顯示全部的編譯變量 int i; zend_compiled_variable *vars = EG(active_op_array)->vars; for(i=0; i < EG(active_op_array)->last_var; ++i) { // last_var 最後一個編譯變量的索引 spprintf(&strg, 0, "%s\\nVar:%s\\n", strg, EG(active_op_array)->vars[i].name); ++vars; } */ zend_op *pre_opline_ptr = *EG(opline_ptr); pre_opline_ptr--; // 支持這類的調用: get_var_name($a="VALUE"); // expect: a // 這裏增長在賦值的狀況下也能正確返回變量的名字的處理方法, 若是方法參數是賦值的的話, 編譯的OPCODE 中SEND_VAR以前將會 // 有一個ZEND_ASSIGN 操做, 而且ZEND_ASSIGN操做的返回值被使用.好比: $c = $d + 1; $d + 1的返回值就被使用了. 就能夠確認 // 是前面的調用方式 zend_op *pre_pre_online_ptr = pre_opline_ptr - 1; if(pre_pre_online_ptr && pre_pre_online_ptr->opcode == ZEND_ASSIGN && !(pre_pre_online_ptr->result.u.EA.type & EXT_TYPE_UNUSED)) { // 經過賦值以前的zend_op來獲取變量信息 pre_opline_ptr = pre_pre_online_ptr; } int index; // 好比get_var_name($name); 這時SEND_VAR OPCODE的op1操做數類型就是IS_CV 也就是IS Compiled Variable // 只有compiled variable纔是直接存儲索引的. PHP >= 5.1 if(pre_opline_ptr->op1.op_type == IS_CV) { index = pre_opline_ptr->op1.u.var; } else { // 請參考VLD的源代碼 $VLD_SRC/srm_oparray.c LINE:320 vld_dump_znode函數 index = pre_opline_ptr->op1.u.var / sizeof(temp_variable); } zend_compiled_variable var = EG(active_op_array)->vars[index]; len = spprintf(&strg, 0, "%s", strg, var.name); RETURN_STRINGL(strg, len, 0); } /* }}} */ |