Zend引擎探索 之 PHP中前置递增不返回左值
首先來講,一般我們對“左值”的理解就是可以出現在賦值運算符的左側的標識符,也就是可以被賦值。這樣講也許并不十分確切,在不同的語言中對左值的定義也不盡相同。在這里我們討論前置遞增(和遞減)運算符的場景下,說前置遞增需要返回左值,更簡明的來講就是要返回變量自身,或者自身的引用。
一、分析問題
在PHP中遇到這個問題,最初是因為寫了類似如下的代碼:
<?php function func01(&$a) {echo $a . PHP_EOL;$a += 10; } $n = 0; func01(++$n); echo $n . PHP_EOL;按照寫C++的經驗,上面代碼應該打印出1和11,但是PHP出乎意料的打印出了1和1。為了一探究竟,我使用zendump擴展中的zendump_opcodes()函數打印出上面代碼的OPCODES:
[root@c962bf018141 php-7.2.2]# sapi/cli/php -f ~/php/func_arg_pre_inc_by_ref.php 1 1 op_array("") refcount(1) addr(0x7f7445c812a0) vars(1) T(5) filename(/root/php/func_arg_pre_inc_by_ref.php) line(1,12) OPCODE OP1 OP2 RESULT EXTENDED ZEND_NOP ZEND_ASSIGN $n 0 ZEND_INIT_FCALL 128 "func01" 1 ZEND_PRE_INC $n #var1 ZEND_SEND_VAR_NO_REF #var1 1 ZEND_DO_UCALL ZEND_CONCAT $n "\n" #tmp3 ZEND_ECHO #tmp3 ZEND_INIT_FCALL 80 "zendump_opcodes" 0 ZEND_DO_ICALL ZEND_RETURN 1通過OPCODES來看,主要問題因該是在ZEND_PRE_INC這條指令上,因為其返回值是#var1而不是$n,因為OPCODE和虛擬機棧上的變量布局是在編譯階段確定的,也就是說Zend引擎在編譯時并沒有使用$n自身作為返回值。通過查看zend_vm_def.h中ZEND_PRE_INC和ZEND_PRE_DEC指令的具體實現,可以發現運行時返回的#var1也并不是$n的引用,而是使用ZVAL_COPY_VALUE和ZVAL_COPY宏進行了值拷貝。
看一下這個2012年的Bug:https://bugs.php.net/bug.php?id=62778,看起來也是一個有C++經驗的開發者提交的。當時是PHP 5.4,至今仍處于Open狀態,看來官方并不準備修復此Bug,或許認為這并不是一個Bug。因為PHP并沒有標準化委員會,也沒有語法白皮書,所以還真不好說這到底是不是一個Bug。正因如此,有的時候遇到一些出乎意料的現象也不好找到權威的資料,只能去研究其實現。
二、嘗試修改
因為不太習慣這種實現,就嘗試著自己進行修改,我在PHP 7.2.2的源碼中對ZEND_PRE_INC和ZEND_PRE_DEC兩條指令做了如下修改,主要思路就是如果左操作數不是引用類型的話,將其轉換為引用類型(ZVAL_MAKE_REF宏會判斷),然后讓指令的結果操作數引用左操作數:
這樣修改主要是受到了Zend引擎實現global和static變量方式的啟發,這非常類似于我們在C++中為一個class重載++運算符,最后要返回自身的引用。我也曾嘗試使用INDIRECT類型指針,但是會引起core dump,似乎INDIRECT類型在Zend引擎中只是被用在某些特定的場景下,不像引用類型這樣得到廣泛支持。
修改完成后使用zend_vm_gen.php重新生成代碼并成功make,再回去執行上面的代碼,確實如預期的輸出了1和11:
使用zendump擴展中的zendump_vars()函數來打印局部變量,可以發現$n確實被轉成了引用類型。
三、驗證修改
現在擔心的就是如此修改會不會引入什么Bug,尤其是PHP會不會有什么特性依賴于不返回左值的那種實現。我在修改過的和未經修改的PHP工程下分別執行了make test,并對結果做了對比,發現確實有兩個test沒有通過:
進一步分析沒有通過的測試代碼,發現這兩個test中都在同一個語句內使用了多個前置遞增運算符,如下所示:
<?php $a[2][3] = 'stdClass'; $a[$i=0][++$i] = new $a[++$i][++$i]; print_r($a);$o = new stdClass; $o->a = new $a[$i=2][++$i]; $o->a->b = new $a[$i=2][++$i]; print_r($o);再次使用zendump_opcodes()函數打印出OPCODES:
[root@c962bf018141 php-7.2.2]# sapi/cli/php -f php/testcase007.php op_array("") refcount(1) addr(0x7fba8347f2a0) vars(3) T(36) filename(/root/php/testcase007.php) line(1,13) OPCODE OP1 OP2 RESULT EXTENDED ZEND_INIT_FCALL 80 "zendump_opcodes" 0 ZEND_DO_ICALL ZEND_FETCH_DIM_W $a 2 #var1 ZEND_ASSIGN_DIM #var1 3 ZEND_OP_DATA "stdClass" ZEND_ASSIGN $i 0 #var3 ZEND_PRE_INC $i #var5 ZEND_PRE_INC $i #var7 ZEND_PRE_INC $i #var9 ZEND_FETCH_DIM_R $a #var7 #var8 ZEND_FETCH_DIM_R #var8 #var9 #var10 ZEND_FETCH_CLASS #var10 #var11 ZEND_NEW #var11 #var12 0 ZEND_DO_FCALL ZEND_FETCH_DIM_W $a #var3 #var4 ZEND_ASSIGN_DIM #var4 #var5 ZEND_OP_DATA #var12 ZEND_INIT_FCALL 96 "print_r" 1 ZEND_SEND_VAR $a 1 ZEND_DO_ICALL ZEND_NEW "stdClass" #var15 0 ZEND_DO_FCALL ZEND_ASSIGN $o #var15 ZEND_ASSIGN $i 2 #var19 ZEND_PRE_INC $i #var21 ZEND_FETCH_DIM_R $a #var19 #var20 ZEND_FETCH_DIM_R #var20 #var21 #var22 ZEND_FETCH_CLASS #var22 #var23 ZEND_NEW #var23 #var24 0 ZEND_DO_FCALL ZEND_ASSIGN_OBJ $o "a" ZEND_OP_DATA #var24 ZEND_ASSIGN $i 2 #var28 ZEND_PRE_INC $i #var30 ZEND_FETCH_DIM_R $a #var28 #var29 ZEND_FETCH_DIM_R #var29 #var30 #var31 ZEND_FETCH_CLASS #var31 #var32 ZEND_NEW #var32 #var33 0 ZEND_DO_FCALL ZEND_FETCH_OBJ_W $o "a" #var26 ZEND_ASSIGN_OBJ #var26 "b" ZEND_OP_DATA #var33 ZEND_INIT_FCALL 96 "print_r" 1 ZEND_SEND_VAR $o 1 ZEND_DO_ICALL ZEND_RETURN 1上面緊跟在ZEND_ASSIGN后面的ZEND_PRE_INC指令和3條緊鄰的ZEND_PRE_INC指令,足夠說明問題。說明Zend引擎在編譯的時候,首先對中括號內的數組下標進行求值,按照從左往右的順序,然后才對外層的表達式進行求值。如果前置遞增運算符返回變量引用的話,像上面這樣賦值之后立刻執行前置遞增指令,或者連續執行3條前置遞增指令,得到的結果操作數都引用同一個變量,值也就都是最后一次遞增后的值,所以后續的邏輯自然就不對了。至于Zend引擎為什么這樣實現,目前我也不得而知,猜測可能是為了讓語法解析器實現起來更加簡單。
總結
為了能讓前置遞增、遞減運算符返回變量引用,還要讓以上特性能夠正常工作,就要修改Zend引擎的編譯器,對于上面這種場景使其按照合理的順序生成指令代碼。但是修改編譯器牽涉太大,會帶來多少問題就更難預期了。所以對于這個問題的探索就暫時告一段落。
就算是能讓前置遞增、遞減運算符返回變量引用,其適用場景也是十分有限的,比如像下面這樣的語句,在PHP中是根本無法通過編譯的,如果不修改編譯器還是無法真正體現返回引用在語法層面帶來的便利。或許我們也可以認為,沒必要為了這不是很常用的語法而引入太多的復雜性。
最后,歡迎訪問我的主頁。
轉載于:https://www.cnblogs.com/youlin/p/php_pre_increment_operator_do_not_return_lvalue.html
總結
以上是生活随笔為你收集整理的Zend引擎探索 之 PHP中前置递增不返回左值的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: tplinkwr703无线打印服务器,T
- 下一篇: wr885n 虚拟服务器,TP-Link