首先来讲,一般我们对“左值”的理解就是可以出现在赋值运算符的左侧的标识符,也就是可以被赋值。这样讲也许并不十分确切,在不同的语言中对左值的定义也不尽相同。在这里我们讨论前置递增(和递减)运算符的场景下,说前置递增需要返回左值,更简明的来讲就是要返回变量自身,或者自身的引用。

一、分析问题

在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中前置递增不返回左值的更多相关文章

  1. postman简单教程,如何在请求中引用上次请求返回的值

    做接口测试,一定会遇到这种情况,需要拿上次请求的值在本次请求中使用,比如,我们去测试一个东西,要去登录才能做其他的操作,需要拿到登录返回数据中的某些字段,比如,token啊等... 如果发一次请求,就 ...

  2. postman的关联,即如何在请求中引用上次请求返回的值

    做接口测试,一定会遇到这种情况,需要拿上次请求的值在本次请求中使用,比如,我们去测试一个东西,要去登录才能做其他的操作,需要拿到登录返回数据中的某些字段,比如,token啊等... 如果发一次请求,就 ...

  3. Sql中联合查询中的”子查询返回的值不止一个“的问题

    在子查询中,如果想实现如下的功能: select lib,count(*),select sum(newsNo) from Table1 group by lib from Tabel1 T1,Tab ...

  4. C++中让人忽视的左值和右值

    前言 为了了解C++11的新特性右值引用,不得不重新认识一下左右值.学习之初,最快的理解,莫过于望文生义了,右值那就是赋值号右边的值,左值就是赋值号左边的值.在中学的数学的学习中,我们理解的是,左值等 ...

  5. 话说C++中的左值、纯右值、将亡值

    写在前面 C++中有“左值”.“右值”的概念,C++11以后,又有了“左值”.“纯右值”.“将亡值”的概念.关于这些概念,许多资料上都有介绍,本文在拾人牙慧的基础上又加入了一些自己的一些理解,同时提出 ...

  6. 对C++11中的`移动语义`与`右值引用`的介绍与讨论

    本文主要介绍了C++11中的移动语义与右值引用, 并且对其中的一些坑做了深入的讨论. 在正式介绍这部分内容之前, 我们先介绍一下rule of three/five原则, 与copy-and-swap ...

  7. 6.PHP内核探索:Zend引擎

    相信很多人都听说过 Zend Engine 这个名词,也有很多人知道 Zend Engine 就是 PHP 语言的核心,但若要问一句:Zend Engine 到底存在于何处?或者说,Zend Engi ...

  8. 第一节 生命周期和Zend引擎

    一切的开始: SAPI接口 SAPI(Server Application Programming Interface)指的是PHP具体应用的编程接口, 就像PC一样,无论安装哪些操作系统,只要满足了 ...

  9. 深入理解PHP内核(二)概览-PHP生命周期与Zend引擎

    本文参考自<深入理解PHP内核>,地址:https://github.com/reeze/tipi 本文链接:http://www.orlion.ml/232/ 1.SAPI接口 SAPI ...

随机推荐

  1. mongodb监控工具mongostat

    mongostat的使用及命令详解 mongostat是mongodb自带的状态检测工具,在命令行下使用,会间隔固定时间获取mongodb的当前运行状态,并输出. 1.常用命令格式: mongosta ...

  2. Css之导航栏学习

    Css: ul { list-style-type:none; margin:; padding:; overflow:hidden; background-color:blue; /*固定在顶部*/ ...

  3. Python——cmd调用(os.system阻塞处理)

    os.system(返回值为0,1,2) 0:成功 1:失败 2:错误 os.system默认阻塞当前程序执行,在cmd命令前加入start可不阻塞当前程序执行. 例如: import os os.s ...

  4. Android 扩大 View 的点击区域

    有时候,按照视觉图做出来效果后,发现点击区域过小,不好点击,用户体验肯定不好.扩大视图,就会导致整个视觉图变得不好看.那么有没有什么办法在不改变视图大小的前提下扩大点击区域呢? 答案是有! 能够解决这 ...

  5. AJAX使用说明书

    AJAX简介 什么是AJAX AJAX(Asynchronous Javascript And XML)翻译成中文就是“异步Javascript和XML”.即使用Javascript语言与服务器进行异 ...

  6. pygame事件之——控制物体(飞机)的移动

    近来想用pygame做做游戏,在 xishui 大神的目光博客中学了学这东西,就上一段自己写的飞机大战的代码,主要是对键盘控制飞机的移动做了相关的优化 # -*- coding: utf-8 -*- ...

  7. Django admin 中抛出 'WSGIRequest' object has no attribute 'user'的错误

    这是Django版本的问题,1.9之前,中间件的key为MIDDLEWARE_CLASSES, 1.9之后,为MIDDLEWARE.所以在开发环境和其他环境的版本不一致时,要特别小心,会有坑. 将se ...

  8. 未能加载文件或程序集“ RevitAPIUI.dll”

    revit二次开发中遇到的问题 RevitAPIUI.dll 只能 Native Library 中执行: 脱离了Native Library,API是跑不起来的 . 检查程序流程:登录,配置,启动r ...

  9. [LuoguP1113] 杂物 - 拓扑排序

    其实只是纪念下第一篇洛谷题解? Description John的农场在给奶牛挤奶前有很多杂务要完成,每一项杂务都需要一定的时间来完成它.比如:他们要将奶牛集合起来,将他们赶进牛棚,为奶牛清洗乳房以及 ...

  10. tr069开源代码——cwmp移植

    原创作品,转载请注明出处,严禁非法转载.如有错误,请留言! email:40879506@qq.com 声明:本系列涉及的开源程序代码学习和研究,严禁用于商业目的. 如有任何问题,欢迎和我交流.(企鹅 ...