經過前面對r2ec以及Bison的介紹,熟悉了PHP語法的實現,我們來動手自己實現一個語法吧。也就是對Zend引擎語法層面的實現。以此來對Zend引擎有更多的了解。
編程語言和社會語言一樣都是會慢慢演進的,不同的語種就像我們的不同國家的語言一樣,他們各有各的特點,語言通常也能反映出一個群體的特質,不同語言的社區氛圍和文化也都會有很大的差異,和現實生活一樣,我們也需要盡可能的去接觸不同的文化,來開闊自己的視野和思維方式,所以我們也建議多學習不同的編程語言。
在這里簡單提一下PHP語言的演進,PHP的語法繼承自Perl的語法,這一點和自然語言也很類似,語言之間會互相影響,比如PHP5開始完善的面向對象機制,已經PHP5.4中增加的命名空間以及閉包等等功能。
PHP是個開源項目,它的發展是由社區來決定的,它也是開放的,如果你有想要改進它的愿望都可以加入到這個社區當中,當然也不是誰都可以改變PHP,重大改進都需要由社區確定,只有有限的人具有對代碼庫的修改權限,如果你發現了PHP的Bug可以去[http://bugs.php.net](http://bugs.php.net)提交Bug,如果同時你也找到了Bug的原因那么你也可以同時附上對Bug的修復補丁,然后在PHP郵件組中進行一些討論,如果沒有問題那么有權限的成員就可以將你的補丁合并進入相應的版本內,更多內容可以參考[附錄D 怎樣為PHP共享自己的力量](#)。
在本小節中將要實現一個對PHP本身語言的一個“需求”:返回變量的名稱。用一小段代碼簡單描述一下這個需求:
[php]
<?php
$demo = 'tipi';
echo var_name($demo); //執行結果,輸出: demo
?>
經過前面的章節,我們了解到,一種PHP語法的內部實現,主要經歷了以下步驟:

圖7.2 Zend Opcodes執行
即:詞法分析 => 語法分析 => opcode編譯 => 執行
由此,我們還是要從詞法和語法分析著手。
### 詞法分析與語法分析[]()
熟悉編譯原理的朋友應該比較熟悉這兩個概念,簡而言之,就是在要運行的程序中,根據原來設定好的“關鍵字”(Tokens),將每條程序指令解釋成為可以由語言解釋器理解的操作。
> 在PHP中,可以使用token_get_all()函數來查看一段PHP代碼生成的Tokens。
PHP的詞法分析和語法分析的實現分別位于Zend目錄下的zend_language_scanner.l和zend_language_parser.y 文件,使用r2ec&flex來編譯。我們要做的,就是在PHP原有的詞法和語法分析中,加入新的Token,在zend_language_scanner.l中加入以下內容:
"var_name" {
return T_VARIABLE_NAME;
}
也就是在此法分析階段遇到var_name這個字符串的時候會被標記為我們定義的T_VARIABLE_NAME token。
同樣,在 zend_language_parser.y 也需要加入對這個token的處理,通常是進行響應的邏輯處理。我們要實現的語法和PHP內置的echo print結構類似,所以我們把這個處理放到 internal_functions_in_yacc規則里面:
| T_VARIABLE_NAME '(' T_VARIABLE ')' { zend_do_variable_name(&$$, &$3 TSRMLS_CC); }
| T_VARIABLE_NAME T_VARIABLE { zend_do_variable_name(&$$, &$2 TSRMLS_CC); }
上面的兩條規則分別對于類似:
<?php
echo var_name($varname);
echo var_name $varname;
的兩種調用方式,和include() require() 類似。
大家可以很容易理解第一行的定義,如果發現T_VARIABLE_NAME + ( + 變量 + ), 則使用zend_do_variable_name來處理,&$$是當前表達式的返回值, &$3表示第三個表達式的值,也就是T_VARIABLE,也就是一個通常的變量定義。這樣就是把變量相關的信息傳遞進zend_do_variable_name()函數中進行處理。在這里是獲取變量的名稱,然后進行opcode編譯。
### opcode編譯[]()
在開始之前需要向大家介紹一下PHP opcode的定義及執行。opcode在PHP中通常是一個數字唯一標示,在PHP中目前對每個opcode對應的執行方法的分發提供了3種方式:
首先,我們在Zend/zend_vm_opcodes.h 為我們的新opcode 加入一個宏定義:
#define ZEND_VARIABLE_NAME 154
這個數字要求在0-255之間,并且不能與現有opcode重復。
第二步,在Zend/zend_compile.c中加入我們對OPCODE的處理,也就是將代碼操作轉化為op_array放入到opline中:
void zend_do_variable_name(znode *result, znode *variable TSRMLS_DC)
{
// 生成一條zend_op
zend_op *opline = get_next_op(CG(active_op_array) TSRMLS_CC);
?
// 因為我們需要有返回值, 并且返回值只作為中間值.所以就是一個臨時變量
opline->result.op_type = IS_TMP_VAR;
opline->result.u.var = get_temporary_variable(CG(active_op_array));
?
opline->opcode = ZEND_VARIABLE_NAME;
opline->op1 = *variable;
?
// 我們只需要一個操作數就好了
SET_UNUSED(opline->op2);
*result = opline->result;
}
這樣,我們就完成了對opcode的編譯。
### 內部處理邏輯的編寫[]()
經過在上面兩個步驟中,我們已經完成了自定義PHP語法的語法規則定義,opcode編譯。最后的工作,就是定義如何處理自定義的opcode,以及編寫具體的代碼邏輯。在前面關于如何找到opcode具體實現的小節,我們提到 Zend/zend_vm_execute.h中的zend_vm_get_opcode_handler()函數。這個函數就是用來獲取opcode的執行函數。
這個對應的關系,是根據一個公式來進行了,目的是將不同的參數類型分開,對應到多個處理函數,公式是這樣的:
return zend_opcode_handlers[opcode * 25 + zend_vm_decode[op->op1.op_type] * 5
+ zend_vm_decode[op->op2.op_type]];
從這個公式我們可以看出,最終的處理函數是與參數類型有關,根據計算,我們要滿足所有類型的映射,盡管我們可以可以使用同一函數進行處理,于是,我們在zend_opcode_handlers這個數組的結尾,加上25個相同的函數定義:
void zend_init_opcodes_handlers(void)
{
static const opcode_handler_t labels[] = {
....
ZEND_VARIABLE_NAME_HANDLER,
....
ZEND_VARIABLE_NAME_HANDLER
}
如果我們不想支持某類型的數據,只需要將類型代入公式計算出的數字做為索引,使opcode_handler_t中相應的項為:ZEND_NULL_HANDLER
最后,我們在Zend/zend_vm_def.h 中增加相應的處理函數。
> 和對語法的修改一樣,opcode處理函數也不是直接修改Zend/zend_vm_execute.h文件的, 這是因為PHP提供了3種opcode分發的機制: 1. CALL 函數調用的方式分發 1. SWITCH 使用SWITCH case 進行分發 1. GOTO 使用goto語句進行分發 之所以提供3中方式主要是從性能出發的,可能在不同的CPU上這幾種調用方式的效率并不一樣。 默認采用的是CALL
回到編寫返回變量名的具體實現,在Zend/zend_vm_def.h中增加如下:
static int ZEND_FASTCALL ZEND_VARIABLE_NAME_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
zend_op *opline = EX(opline);
?
// PHP中所有的變量在內部都是存儲在zval結構中的.
zval *result = &EX_T(opline->result.u.var).tmp_var;
?
// 把變量的名字賦給臨時返回值
Z_STRVAL(*result) = estrndup(opline->op1.u.constant.value.str.val, opline->op1.u.constant.value.str.len);
Z_STRLEN(*result) = opline->op1.u.constant.value.str.len;
Z_TYPE(EX_T(opline->result.u.var).tmp_var) = IS_STRING;
?
ZEND_VM_NEXT_OPCODE();
}
進行完上面的修改之后,我們要刪除r2ec&flex已經編譯好的原文件,即刪除Zend/zend_language*.c文件以使新的語法規則生效。這樣我們再次對PHP源碼進行make時,會自動生成新的編譯好的語法規則處理程序,不過,編譯環境要安裝有lex&yacc和re2c。
從上面的步驟可以看出,php語法的擴展并不困難,而真正的難點在于如何在當前zend內核框架基礎上進行的具體功能的實現,以及到底應該實現什么語法。關于語法的改進通常也是一個漫長的過程,要修改語言的語法通常需要:
1. 提出需求,并說明該語法的作用,以及具體的應用場景,這個語法帶來的好處
1. 大家討論這個需求是否合理,實現啊起來是否有困難,對現有的語法是否造成影響
1. 如果大部分人都認可這個需求最好,那么提出該需求的人可以自己來實現,并讓大家review,如果沒有問題則就可以進入版本庫了。
1. 如果比較有爭議,那可能需要進行投票了。
更多內容請參考附錄:怎么樣為PHP做貢獻小節。
- 第一章 準備工作和背景知識
- 第一節 環境搭建
- 第二節 源碼結構、閱讀代碼方法
- 第三節 常用代碼
- 第四節 小結
- 第二章 用戶代碼的執行
- 第一節 生命周期和Zend引擎
- 第二節 SAPI概述
- Apache模塊
- 嵌入式
- FastCGI
- 第三節 PHP腳本的執行
- 詞法分析和語法分析
- opcode
- opcode處理函數查找
- 第四節 小結
- 第三章 變量及數據類型
- 第一節 變量的結構和類型
- 哈希表(HashTable)
- PHP的哈希表實現
- 鏈表簡介
- 第二節 常量
- 第三節 預定義變量
- 第四節 靜態變量
- 第五節 類型提示的實現
- 第六節 變量的生命周期
- 變量的賦值和銷毀
- 變量的作用域
- global語句
- 第七節 數據類型轉換
- 第八節 小結
- 第四章 函數的實現
- 第一節 函數的內部結構
- 函數的內部結構
- 函數間的轉換
- 第二節 函數的定義,傳參及返回值
- 函數的定義
- 函數的參數
- 函數的返回值
- 第三節 函數的調用和執行
- 第四節 匿名函數及閉包
- 第五節 小結
- 第五章 類和面向對象
- 第一節 類的結構和實現
- 第二節 類的成員變量及方法
- 第三節 訪問控制的實現
- 第四節 類的繼承,多態及抽象類
- 第五節 魔術方法,延遲綁定及靜態成員
- 第六節 PHP保留類及特殊類
- 第七節 對象
- 第八節 命名空間
- 第九節 標準類
- 第十節 小結
- 第六章 內存管理
- 第一節 內存管理概述
- 第二節 PHP中的內存管理
- 第三節 內存使用:申請和銷毀
- 第四節 垃圾回收
- 新的垃圾回收
- 第五節 內存管理中的緩存
- 第六節 寫時復制(Copy On Write)
- 第七節 內存泄漏
- 第八節 小結
- 第七章 Zend虛擬機
- 第一節 Zend虛擬機概述
- 第二節 語法的實現
- 詞法解析
- 語法分析
- 實現自己的語法
- 第三節 中間代碼的執行
- 第四節 PHP代碼的加密解密
- 第五節 小結
- 第八章 線程安全
- 第二節 線程,進程和并發
- 第三節 PHP中的線程安全
- 第九章 錯誤和異常處理
- 第十章 輸出緩沖
- 第十六章 PHP語言特性的實現
- 第一節 循環語句
- foreach的實現
- 第二十章 怎么樣系列(how to)
- 附錄
- 附錄A PHP及Zend API
- 附錄B PHP的歷史
- 附錄C VLD擴展使用指南
- 附錄D 怎樣為PHP貢獻
- 附錄E phpt測試文件說明
- 附錄F PHP5.4新功能升級解析
- 附錄G:re2c中文手冊