[原创]从php源码研究empty()与SimpleXMLElement的特殊现象

在SimpleXML拓展的使用中,发现了一个奇怪的小细节,最简化的代码如下:

$xmlStr=simplexml_load_string('<class><teacher>张三</teacher></class>');
var_dump($xmlStr->student);//object(SimpleXMLElement)#2 (0) { } 
$haveStudent=(empty($xmlStr->student));
var_dump($haveStudent);//true

初看这非常合理,但是回想一下php官方文档empty()的两段说明

empty() 本质上与 !isset($var) || $var == false 等价

以下的东西被认为是empty的:

  • (空字符串)
  • 0 (作为整数的0)
  • 0.0 (作为浮点数的0)
  • "0" (作为字符串的0)
  • NULL
  • FALSE
  • array() (一个空数组)
  • $var; (一个声明了,但是没有值的变量)

咦?一般非null对象转bool结果都是true,这个情况也不在enpty指定的列表中,为何该SimpleXMLElement节点empty()结果为true?这难道是什么特殊的BUG?

Zend BUG report https://bugs.php.net/bug.php?id=30972&edit=1
PHP文档:Bool的转换 http://php.net/manual/zh/language.types.boolean.php
根据上面两个文档可以看出,其实我已经不是第一个有这种疑惑的人了,并且SimpleXml的处理已经被官方定位为feature而不是BUG了,甚至在bool的转换文档中成为了唯一的一个特例。
但是,这到底是如何导致的呢?出于疑惑,我翻了下empty()的相关源码。

如何找到empty的源码?

opline指令是ZendVM的执行指令,而opline的handler成员就是ZendVM在执行阶段真正的执行内容。
严谨的来说,因为empty结构可能对应不止一种opline,所以我们需要从语法和词法分析开始,追溯生成的对应ast和ast编译出的opline,再根据opline的opcode和2个操作数类型使用zend_opcode_handlers[]获取empty()可能的所有handler.
但我们的目的并不是学习empty()的完整实现,而是搞清楚这个现象的直接原因,所以这里使用了一种更加巧妙和快速的办法。

zend_execute()是php脚本执行的入口函数,其参数op_array是编译器最终输出的内容,op_array的opcodes成员即上文提到的opline的集合。所以利用gdb在zend_execute()处打断点即可得到当前PHP脚本的所有执行句柄。

示例代码实测执行句柄如下:

(int (*)(zend_execute_data *)) 0x89c39f <ZEND_INIT_FCALL_SPEC_CONST_HANDLER>
(int (*)(zend_execute_data *)) 0x89ecf3 <ZEND_SEND_VAL_SPEC_CONST_HANDLER>
(int (*)(zend_execute_data *)) 0x8974a3 <ZEND_DO_ICALL_SPEC_HANDLER>
(int (*)(zend_execute_data *)) 0x8e681d <ZEND_ASSIGN_SPEC_CV_VAR_HANDLER>
(int (*)(zend_execute_data *)) 0x89c39f <ZEND_INIT_FCALL_SPEC_CONST_HANDLER>
(int (*)(zend_execute_data *)) 0x8e14b4 <ZEND_FETCH_OBJ_R_SPEC_CV_CONST_HANDLER>
(int (*)(zend_execute_data *)) 0x8b875d <ZEND_SEND_VAR_SPEC_VAR_HANDLER>
(int (*)(zend_execute_data *)) 0x8974a3 <ZEND_DO_ICALL_SPEC_HANDLER>
(int (*)(zend_execute_data *)) 0x8e14b4 <ZEND_FETCH_OBJ_R_SPEC_CV_CONST_HANDLER>
(int (*)(zend_execute_data *)) 0x8e681d <ZEND_ASSIGN_SPEC_CV_VAR_HANDLER>
(int (*)(zend_execute_data *)) 0x8e499d <ZEND_ISSET_ISEMPTY_PROP_OBJ_SPEC_CV_CONST_HANDLER>
(int (*)(zend_execute_data *)) 0x8e5b24 <ZEND_ASSIGN_SPEC_CV_TMP_HANDLER>
(int (*)(zend_execute_data *)) 0x89c39f <ZEND_INIT_FCALL_SPEC_CONST_HANDLER>
(int (*)(zend_execute_data *)) 0x8d9c85 <ZEND_SEND_VAR_SPEC_CV_HANDLER>
(int (*)(zend_execute_data *)) 0x8974a3 <ZEND_DO_ICALL_SPEC_HANDLER>
(int (*)(zend_execute_data *)) 0x89e9b8 <ZEND_RETURN_SPEC_CONST_HANDLER>

显而易见的,ZEND_ISSET_ISEMPTY_PROP_OBJ_SPEC_CV_CONST_HANDLER就是我们想要的empty的handler。
笔者公司开发环境用的php版本为php7.013,此时源码如下:

static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_ISSET_ISEMPTY_PROP_OBJ_SPEC_CV_CONST_HANDLER(ZEND_OPCODE_HANDLER_ARGS){
    USE_OPLINE

    zval *container;
    int result;
    zval *offset;

    SAVE_OPLINE();
    //操作数1,偏移转zval指针操作(zval在内核中对应php的一个变量,下文不再赘叙),container此处等同$xmlStr
    container = _get_zval_ptr_cv_BP_VAR_IS(execute_data, opline->op1.var);

    //下面前两个if分支是自动生成的,会被编译器被直接优化掉,忽略之
    if (IS_CV == IS_UNUSED && UNEXPECTED(Z_OBJ_P(container) == NULL)) {
        zend_throw_error(NULL, "Using $this when not in object context");

        HANDLE_EXCEPTION();
    }

    offset = EX_CONSTANT(opline->op2);//操作数2,offset此处即'student'

    if (IS_CV == IS_CONST ||
        (IS_CV != IS_UNUSED && UNEXPECTED(Z_TYPE_P(container) != IS_OBJECT))) {
        if ((IS_CV & (IS_VAR|IS_CV)) && Z_ISREF_P(container)) {
            container = Z_REFVAL_P(container);
            if (UNEXPECTED(Z_TYPE_P(container) != IS_OBJECT)) {
                goto isset_no_object;
            }
        } else {
            goto isset_no_object;
        }
    }
    //
    if (UNEXPECTED(!Z_OBJ_HT_P(container)->has_property)) {
        //如果container持有的zend_object(内核中对应php的一个对象值)没有has_property函数句柄
        //抛出一个E_NOTICE等级的错误,我们没报错,不可能是这条if分支
        zend_error(E_NOTICE, "Trying to check property of non-object");
isset_no_object:
        result = ((opline->extended_value & ZEND_ISSET) == 0);
    } else {
        //实际执行的分支
        result =
        //这个handler是isset和empty结构公用的,当ast为ZEND_AST_ISSET时 
        //(opline->extended_value & ZEND_ISSET)不为0(详情见zend_compile_isset_or_empty()) 
            ((opline->extended_value & ZEND_ISSET) == 0) ^
            Z_OBJ_HT_P(container)->has_property(container, offset, (opline->extended_value & ZEND_ISSET) == 0, ((IS_CONST == IS_CONST) ? CACHE_ADDR(Z_CACHE_SLOT_P(offset)) : NULL));
    }


    ZEND_VM_SMART_BRANCH(result, 1);
    ZVAL_BOOL(EX_VAR(opline->result.var), result);
    ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION();
}

看到这里基本上原因已经猜个八九不离时,empty()的逻辑不是固定的,而是在此处调用了对象实际的has_property()去计算empty的返回值,只要SimpleXMLElement类改动了该方法,就可能会导致开头出现的情况。

static zend_object_handlers sxe_object_handlers = { /* {{{ */
    ZEND_OBJECTS_STORE_HANDLERS,
    sxe_property_read,
    sxe_property_write,
    sxe_dimension_read,
    sxe_dimension_write,
    sxe_property_get_adr,
    sxe_get_value,          /* get */
    NULL,
    sxe_property_exists,//对应has_property成员
    sxe_property_delete,
    sxe_dimension_exists,
    sxe_dimension_delete,
    sxe_get_properties,
    NULL, /* zend_get_std_object_handlers()->get_method,*/
    NULL, /* zend_get_std_object_handlers()->call_method,*/
    NULL, /* zend_get_std_object_handlers()->get_constructor, */
    NULL, /* zend_get_std_object_handlers()->get_class_name,*/
    sxe_objects_compare,
    sxe_object_cast,
    sxe_count_elements,
    sxe_get_debug_info,
    NULL,
    sxe_get_gc
};

回头瞄一下SimpleXML拓展的源码,果不其然。SimpleXMLElement正正替换了原生的zend_object_handlers以重写类的基本功能,其中就包括了has_property(),所以我们对empty()的一般认识才在此处失效了。

最后:
这种和文档描述不一致的现象,与其说它说明PHP文档的不严谨,不如说这正说明了PHP/Zend拓展是一类如何强大而危险的工具。回想一下opcode和xdebug之类的黑科技拓展,人家是连zend_compile_file指针都去替换的。
况且从设计的角度来看,ZendVM预留出种种诸如zend_object_handlers 这样的灵活结构去解耦和加强灵活性,正是为了让我们更进一步的去拓展。
仅仅有处理用户自定义函数和类的知识,真的不足以让我们驰骋PHP。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342

推荐阅读更多精彩内容

  • 内存是计算机非常关键的部件之一,是暂时存储程序以及数据的空间,CPU只有有限的寄存器可以用于 存储计算数据,而大部...
    dreamer_lk阅读 1,174评论 2 10
  • 2017最后一百天,我已经确定自己要做什么了。先分享最近自己经历的几个真事。 第一件事是我又换工作了。工作十年,前...
    米兰默阅读 642评论 0 0
  • 1 简介 单例模式是一种对象创建型模式。单例模式保证一个类只有一个实例存在,并且同时提供一个全局的方法进行进行访问...
    司鑫阅读 416评论 0 6
  • ​ 人间小剧场第二幕 不能原谅的 被背叛过一次就要立刻甩了他 不然他就会一直恃宠而骄的 鼓励男人, 对他好,容忍他...
    一加雨录阅读 4,025评论 0 0