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-..]# sapi/cli/php -f ~/php/func_arg_pre_inc_by_ref.php
op_array("") refcount() addr(0x7f7445c812a0) vars() T() filename(/root/php/func_arg_pre_inc_by_ref.php) line(,)
OPCODE OP1 OP2 RESULT EXTENDED
ZEND_NOP
ZEND_ASSIGN $n
ZEND_INIT_FCALL "func01"
ZEND_PRE_INC $n #var1
ZEND_SEND_VAR_NO_REF #var1
ZEND_DO_UCALL
ZEND_CONCAT $n "\n" #tmp3
ZEND_ECHO #tmp3
ZEND_INIT_FCALL "zendump_opcodes"
ZEND_DO_ICALL
ZEND_RETURN
通过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宏会判断),然后让指令的结果操作数引用左操作数:
[root@c962bf018141 Zend]# diff zend_vm_def.h zend_vm_def.h.bak
1211c1211
< zval *var_ptr, *varptr;
---
> zval *var_ptr;
1213c1213
< varptr = var_ptr = GET_OP1_ZVAL_PTR_PTR_UNDEF(BP_VAR_RW);
---
> var_ptr = GET_OP1_ZVAL_PTR_PTR_UNDEF(BP_VAR_RW);
,1219c1218
< ZVAL_MAKE_REF(varptr);
< ZVAL_COPY(EX_VAR(opline->result.var), varptr);
---
> ZVAL_COPY_VALUE(EX_VAR(opline->result.var), var_ptr);
,1242c1240
< ZVAL_MAKE_REF(varptr);
< ZVAL_COPY(EX_VAR(opline->result.var), varptr);
---
> ZVAL_COPY(EX_VAR(opline->result.var), var_ptr);
1253c1251
< zval *var_ptr, *varptr;
---
> zval *var_ptr;
1255c1253
< varptr = var_ptr = GET_OP1_ZVAL_PTR_PTR_UNDEF(BP_VAR_RW);
---
> var_ptr = GET_OP1_ZVAL_PTR_PTR_UNDEF(BP_VAR_RW);
,1261c1258
< ZVAL_MAKE_REF(varptr);
< ZVAL_COPY(EX_VAR(opline->result.var), varptr);
---
> ZVAL_COPY_VALUE(EX_VAR(opline->result.var), var_ptr);
,1284c1280
< ZVAL_MAKE_REF(varptr);
< ZVAL_COPY(EX_VAR(opline->result.var), varptr);
---
> ZVAL_COPY(EX_VAR(opline->result.var), var_ptr);
这样修改主要是受到了Zend引擎实现global和static变量方式的启发,这非常类似于我们在C++中为一个class重载++运算符,最后要返回自身的引用。我也曾尝试使用INDIRECT类型指针,但是会引起core dump,似乎INDIRECT类型在Zend引擎中只是被用在某些特定的场景下,不像引用类型这样得到广泛支持。
修改完成后使用zend_vm_gen.php重新生成代码并成功make,再回去执行上面的代码,确实如预期的输出了1和11:
[root@c962bf018141 php-7.2.]# sapi/cli/php -f ~/php/func_arg_pre_inc_by_ref.php
vars(): {
$n ->
zval(0x7fd182c1d080) -> reference() addr(0x7fd182c5f078) zval(0x7fd182c5f080) : long()
}
使用zendump扩展中的zendump_vars()函数来打印局部变量,可以发现$n确实被转成了引用类型。
三、验证修改
现在担心的就是如此修改会不会引入什么Bug,尤其是PHP会不会有什么特性依赖于不返回左值的那种实现。我在修改过的和未经修改的PHP工程下分别执行了make test,并对结果做了对比,发现确实有两个test没有通过:
Check key execution order with new. [tests/lang/engine_assignExecutionOrder_007.phpt]
Execution ordering with comparison operators. [tests/lang/engine_assignExecutionOrder_009.phpt]
进一步分析没有通过的测试代码,发现这两个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.]# sapi/cli/php -f php/testcase007.php
op_array("") refcount() addr(0x7fba8347f2a0) vars() T() filename(/root/php/testcase007.php) line(,)
OPCODE OP1 OP2 RESULT EXTENDED
ZEND_INIT_FCALL "zendump_opcodes"
ZEND_DO_ICALL
ZEND_FETCH_DIM_W $a #var1
ZEND_ASSIGN_DIM #var1
ZEND_OP_DATA "stdClass"
ZEND_ASSIGN $i #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
ZEND_DO_FCALL
ZEND_FETCH_DIM_W $a #var3 #var4
ZEND_ASSIGN_DIM #var4 #var5
ZEND_OP_DATA #var12
ZEND_INIT_FCALL "print_r"
ZEND_SEND_VAR $a
ZEND_DO_ICALL
ZEND_NEW "stdClass" #var15
ZEND_DO_FCALL
ZEND_ASSIGN $o #var15
ZEND_ASSIGN $i #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
ZEND_DO_FCALL
ZEND_ASSIGN_OBJ $o "a"
ZEND_OP_DATA #var24
ZEND_ASSIGN $i #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
ZEND_DO_FCALL
ZEND_FETCH_OBJ_W $o "a" #var26
ZEND_ASSIGN_OBJ #var26 "b"
ZEND_OP_DATA #var33
ZEND_INIT_FCALL "print_r"
ZEND_SEND_VAR $o
ZEND_DO_ICALL
ZEND_RETURN
上面紧跟在ZEND_ASSIGN后面的ZEND_PRE_INC指令和3条紧邻的ZEND_PRE_INC指令,足够说明问题。说明Zend引擎在编译的时候,首先对中括号内的数组下标进行求值,按照从左往右的顺序,然后才对外层的表达式进行求值。如果前置递增运算符返回变量引用的话,像上面这样赋值之后立刻执行前置递增指令,或者连续执行3条前置递增指令,得到的结果操作数都引用同一个变量,值也就都是最后一次递增后的值,所以后续的逻辑自然就不对了。至于Zend引擎为什么这样实现,目前我也不得而知,猜测可能是为了让语法解析器实现起来更加简单。
总结
为了能让前置递增、递减运算符返回变量引用,还要让以上特性能够正常工作,就要修改Zend引擎的编译器,对于上面这种场景使其按照合理的顺序生成指令代码。但是修改编译器牵涉太大,会带来多少问题就更难预期了。所以对于这个问题的探索就暂时告一段落。
就算是能让前置递增、递减运算符返回变量引用,其适用场景也是十分有限的,比如像下面这样的语句,在PHP中是根本无法通过编译的,如果不修改编译器还是无法真正体现返回引用在语法层面带来的便利。或许我们也可以认为,没必要为了这不是很常用的语法而引入太多的复杂性。
$b = &++$a;
++$a += 10;
++(++$b);
最后,欢迎访问我的主页。
Zend引擎探索 之 PHP中前置递增不返回左值的更多相关文章
- postman简单教程,如何在请求中引用上次请求返回的值
做接口测试,一定会遇到这种情况,需要拿上次请求的值在本次请求中使用,比如,我们去测试一个东西,要去登录才能做其他的操作,需要拿到登录返回数据中的某些字段,比如,token啊等... 如果发一次请求,就 ...
- postman的关联,即如何在请求中引用上次请求返回的值
做接口测试,一定会遇到这种情况,需要拿上次请求的值在本次请求中使用,比如,我们去测试一个东西,要去登录才能做其他的操作,需要拿到登录返回数据中的某些字段,比如,token啊等... 如果发一次请求,就 ...
- Sql中联合查询中的”子查询返回的值不止一个“的问题
在子查询中,如果想实现如下的功能: select lib,count(*),select sum(newsNo) from Table1 group by lib from Tabel1 T1,Tab ...
- C++中让人忽视的左值和右值
前言 为了了解C++11的新特性右值引用,不得不重新认识一下左右值.学习之初,最快的理解,莫过于望文生义了,右值那就是赋值号右边的值,左值就是赋值号左边的值.在中学的数学的学习中,我们理解的是,左值等 ...
- 话说C++中的左值、纯右值、将亡值
写在前面 C++中有“左值”.“右值”的概念,C++11以后,又有了“左值”.“纯右值”.“将亡值”的概念.关于这些概念,许多资料上都有介绍,本文在拾人牙慧的基础上又加入了一些自己的一些理解,同时提出 ...
- 对C++11中的`移动语义`与`右值引用`的介绍与讨论
本文主要介绍了C++11中的移动语义与右值引用, 并且对其中的一些坑做了深入的讨论. 在正式介绍这部分内容之前, 我们先介绍一下rule of three/five原则, 与copy-and-swap ...
- 6.PHP内核探索:Zend引擎
相信很多人都听说过 Zend Engine 这个名词,也有很多人知道 Zend Engine 就是 PHP 语言的核心,但若要问一句:Zend Engine 到底存在于何处?或者说,Zend Engi ...
- 第一节 生命周期和Zend引擎
一切的开始: SAPI接口 SAPI(Server Application Programming Interface)指的是PHP具体应用的编程接口, 就像PC一样,无论安装哪些操作系统,只要满足了 ...
- 深入理解PHP内核(二)概览-PHP生命周期与Zend引擎
本文参考自<深入理解PHP内核>,地址:https://github.com/reeze/tipi 本文链接:http://www.orlion.ml/232/ 1.SAPI接口 SAPI ...
随机推荐
- JAVA_SE基础——28.封装
黑马程序员blog... 面向对象三大特征:1. 封装2. 继承3 多态. 今天我们先学习第一大特征,封装. 封装:是指隐藏对象的属性和实现细节,仅对外提供公共访问方式. 好处: 1. 将变 ...
- 使用IDEA快速插入数据库数据的方法
如上图所示:数据库创建表主键使用了自增列自增因此忽略,只有后两列非主键得数据,在数据较多得时候使用IDEA快捷键Ctrl+R键,快速查找替换.
- 算法题丨3Sum Closest
描述 Given an array S of n integers, find three integers in S such that the sum is closest to a given ...
- ssh_maven之controller层开发
我们已经完成了前两层的开发,现在 只剩下我们的controller层了,对于这一层,我们需要创建一个动作类CustomerAction,另外就是我们的strutss.xml以及我们的applicati ...
- PV 动态供给 - 每天5分钟玩转 Docker 容器技术(153)
前面的例子中,我们提前创建了 PV,然后通过 PVC 申请 PV 并在 Pod 中使用,这种方式叫做静态供给(Static Provision). 与之对应的是动态供给(Dynamical Provi ...
- HTML中的上下标标签的演示
HTML中的上下标标签的演示 #table_head>td { font-weight: bold } tr { text-align: center } 作用 标签 演示代码 呈现效果 上标 ...
- ASC学习笔记
TCL:(Tool Command Language), a computer programming languagecharm++:基于C++的面向对象的并行编程语言.Charm++ is a p ...
- JS中的数据类型和转换
一.JS中的数据类型 js中的数据类型可以分为五种:number .string .boolean. underfine .null. number:数字类型 ,整型浮点型都包括. string:字符 ...
- PHP 7.2 新功能介绍
PHP 7.2 已經在 2017 年 11 月 30 日 正式發布 .這次發布包含新特性.功能,及優化,以讓我們寫出更好的代碼.在這篇文章裡,我將會介紹一些 PHP 7.2 最有趣的語言特性. 你可以 ...
- tkinter的冷却技能
validatecommand=(f,s1,s2,s3) f就是冷却后的验证函数名,s1,s2,s3这些时额外的选项,这些选项会作为参数依次传给f函数. register()冷却作用:register ...