Zend引擎探索 之 PHP中前置递增不返回左值
首先來(lái)講,一般我們對(duì)“左值”的理解就是可以出現(xiàn)在賦值運(yùn)算符的左側(cè)的標(biāo)識(shí)符,也就是可以被賦值。這樣講也許并不十分確切,在不同的語(yǔ)言中對(duì)左值的定義也不盡相同。在這里我們討論前置遞增(和遞減)運(yùn)算符的場(chǎng)景下,說(shuō)前置遞增需要返回左值,更簡(jiǎn)明的來(lái)講就是要返回變量自身,或者自身的引用。
一、分析問(wèn)題
在PHP中遇到這個(gè)問(wèn)題,最初是因?yàn)閷?xiě)了類(lèi)似如下的代碼:
<?php function func01(&$a) {echo $a . PHP_EOL;$a += 10; } $n = 0; func01(++$n); echo $n . PHP_EOL;按照寫(xiě)C++的經(jīng)驗(yàn),上面代碼應(yīng)該打印出1和11,但是PHP出乎意料的打印出了1和1。為了一探究竟,我使用zendump擴(kuò)展中的zendump_opcodes()函數(shù)打印出上面代碼的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通過(guò)OPCODES來(lái)看,主要問(wèn)題因該是在ZEND_PRE_INC這條指令上,因?yàn)槠浞祷刂凳?var1而不是$n,因?yàn)镺PCODE和虛擬機(jī)棧上的變量布局是在編譯階段確定的,也就是說(shuō)Zend引擎在編譯時(shí)并沒(méi)有使用$n自身作為返回值。通過(guò)查看zend_vm_def.h中ZEND_PRE_INC和ZEND_PRE_DEC指令的具體實(shí)現(xiàn),可以發(fā)現(xiàn)運(yùn)行時(shí)返回的#var1也并不是$n的引用,而是使用ZVAL_COPY_VALUE和ZVAL_COPY宏進(jìn)行了值拷貝。
看一下這個(gè)2012年的Bug:https://bugs.php.net/bug.php?id=62778,看起來(lái)也是一個(gè)有C++經(jīng)驗(yàn)的開(kāi)發(fā)者提交的。當(dāng)時(shí)是PHP 5.4,至今仍處于Open狀態(tài),看來(lái)官方并不準(zhǔn)備修復(fù)此Bug,或許認(rèn)為這并不是一個(gè)Bug。因?yàn)镻HP并沒(méi)有標(biāo)準(zhǔn)化委員會(huì),也沒(méi)有語(yǔ)法白皮書(shū),所以還真不好說(shuō)這到底是不是一個(gè)Bug。正因如此,有的時(shí)候遇到一些出乎意料的現(xiàn)象也不好找到權(quán)威的資料,只能去研究其實(shí)現(xiàn)。
二、嘗試修改
因?yàn)椴惶?xí)慣這種實(shí)現(xiàn),就嘗試著自己進(jìn)行修改,我在PHP 7.2.2的源碼中對(duì)ZEND_PRE_INC和ZEND_PRE_DEC兩條指令做了如下修改,主要思路就是如果左操作數(shù)不是引用類(lèi)型的話,將其轉(zhuǎn)換為引用類(lèi)型(ZVAL_MAKE_REF宏會(huì)判斷),然后讓指令的結(jié)果操作數(shù)引用左操作數(shù):
這樣修改主要是受到了Zend引擎實(shí)現(xiàn)global和static變量方式的啟發(fā),這非常類(lèi)似于我們?cè)贑++中為一個(gè)class重載++運(yùn)算符,最后要返回自身的引用。我也曾嘗試使用INDIRECT類(lèi)型指針,但是會(huì)引起core dump,似乎INDIRECT類(lèi)型在Zend引擎中只是被用在某些特定的場(chǎng)景下,不像引用類(lèi)型這樣得到廣泛支持。
修改完成后使用zend_vm_gen.php重新生成代碼并成功make,再回去執(zhí)行上面的代碼,確實(shí)如預(yù)期的輸出了1和11:
使用zendump擴(kuò)展中的zendump_vars()函數(shù)來(lái)打印局部變量,可以發(fā)現(xiàn)$n確實(shí)被轉(zhuǎn)成了引用類(lèi)型。
三、驗(yàn)證修改
現(xiàn)在擔(dān)心的就是如此修改會(huì)不會(huì)引入什么Bug,尤其是PHP會(huì)不會(huì)有什么特性依賴(lài)于不返回左值的那種實(shí)現(xiàn)。我在修改過(guò)的和未經(jīng)修改的PHP工程下分別執(zhí)行了make test,并對(duì)結(jié)果做了對(duì)比,發(fā)現(xiàn)確實(shí)有兩個(gè)test沒(méi)有通過(guò):
進(jìn)一步分析沒(méi)有通過(guò)的測(cè)試代碼,發(fā)現(xiàn)這兩個(gè)test中都在同一個(gè)語(yǔ)句內(nèi)使用了多個(gè)前置遞增運(yùn)算符,如下所示:
<?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()函數(shù)打印出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指令,足夠說(shuō)明問(wèn)題。說(shuō)明Zend引擎在編譯的時(shí)候,首先對(duì)中括號(hào)內(nèi)的數(shù)組下標(biāo)進(jìn)行求值,按照從左往右的順序,然后才對(duì)外層的表達(dá)式進(jìn)行求值。如果前置遞增運(yùn)算符返回變量引用的話,像上面這樣賦值之后立刻執(zhí)行前置遞增指令,或者連續(xù)執(zhí)行3條前置遞增指令,得到的結(jié)果操作數(shù)都引用同一個(gè)變量,值也就都是最后一次遞增后的值,所以后續(xù)的邏輯自然就不對(duì)了。至于Zend引擎為什么這樣實(shí)現(xiàn),目前我也不得而知,猜測(cè)可能是為了讓語(yǔ)法解析器實(shí)現(xiàn)起來(lái)更加簡(jiǎn)單。
總結(jié)
為了能讓前置遞增、遞減運(yùn)算符返回變量引用,還要讓以上特性能夠正常工作,就要修改Zend引擎的編譯器,對(duì)于上面這種場(chǎng)景使其按照合理的順序生成指令代碼。但是修改編譯器牽涉太大,會(huì)帶來(lái)多少問(wèn)題就更難預(yù)期了。所以對(duì)于這個(gè)問(wèn)題的探索就暫時(shí)告一段落。
就算是能讓前置遞增、遞減運(yùn)算符返回變量引用,其適用場(chǎng)景也是十分有限的,比如像下面這樣的語(yǔ)句,在PHP中是根本無(wú)法通過(guò)編譯的,如果不修改編譯器還是無(wú)法真正體現(xiàn)返回引用在語(yǔ)法層面帶來(lái)的便利。或許我們也可以認(rèn)為,沒(méi)必要為了這不是很常用的語(yǔ)法而引入太多的復(fù)雜性。
最后,歡迎訪問(wèn)我的主頁(yè)。
轉(zhuǎn)載于:https://www.cnblogs.com/youlin/p/php_pre_increment_operator_do_not_return_lvalue.html
總結(jié)
以上是生活随笔為你收集整理的Zend引擎探索 之 PHP中前置递增不返回左值的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: tplinkwr703无线打印服务器,T
- 下一篇: php7模拟,认识PHP7虚拟机()三