奥门新浦京官方网站PHP实现强类型函数返回值

在开发过程中,函数的返回值类型应该是确定不变的,但PHP是弱类型的语言,

本文只探讨纯粹的函数,并不包含方法。对于方法,会放到类、对象中一起研究。

前言: php4中引入了foreach结构,这是一种遍历数组的简单方式。相比传统的for循环,foreach能够更加便捷的获取键值对。在php5之前,foreach仅能用于数组;php5之后,利用foreach还能遍历对象(详见:遍历对象)。本文中仅讨论遍历数组的情况。

所以PHP是没有此类语法验证的,正因为如此,造成了很多坑坑。

想讲清楚在zend
vm中,函数如何被正确的编译成op指令、如何发生参数传递、如何模拟调用栈、如何切换作用域等等,的确是一个很大范畴的话题。但为了弄明白php的原理,必须要攻克它。

foreach虽然简单,不过它可能会出现一些意外的行为,特别是代码涉及引用的情况下。
下面列举了几种case,有助于我们进一步认清foreach的本质。
问题1: 复制代码 代码如下:
$arr = array(1,2,3);
foreach($arr as $k => &$v) {
    $v = $v * 2;
}
// now $arr is array(2, 4, 6)
foreach($arr as $k => $v) {
    echo “$k”, ” => “, “$v”;
}

比如下面的代码:

对函数的研究,大致可以分成两块。第一块是函数体的编译,主要涉及到如何将函数转化成zend_op指令。第二块是研究函数的调用,涉及到函数调用语句的编译,以及函数如何被执行等topic。这里先来看看函数如何被编译,我们下一篇再讲函数的调用。

先从简单的开始,如果我们尝试运行上述代码,就会发现最后输出为0=>2 
1=>4  2=>4 。
为何不是0=>2  1=>4  2=>6 ?
其实,我们可以认为 foreach($arr as $k => $v)
结构隐含了如下操作,分别将数组当前的’键’和当前的’值’赋给变量$k和$v。具体展开形如:
复制代码 代码如下:
foreach($arr as $k => $v){
    //在用户代码执行之前隐含了2个赋值操作
    $v = currentVal();
    $k = currentKey();
    //继续运行用户代码
    ……
}

<?php
function getArticles(…){
$arrData = array();
if($exp1){
return $arrData;
}else if($exp2){
return 1;
}else{
return false;
}

}
$arrData =getArticles(…);
foreach($arrData as $record){
//do something.
….
}
?>

函数的编译

对函数进行编译,最终目的是为了生成一份对应的zend op指令集,除了zend
op指令集,编译函数还会产生其他一些相关的数据,比如说函数名称、参数列表信息、compiled
variables,甚至函数所在文件、起始行数等等。这些信息作为编译的产出,都需要保存起来。

保存这些编译产出的数据结构,正是上一节中所描述的zend_op_array。在这个系列的文章中,均会以op_array作为简称。

下面列出了一个简单的例子:

<?phpfunction foo($arg1){    print($arg1);}$bar = 'hello php';foo($bar);

这段代码包含了一个最简单的函数示例。

在这样一份php脚本中,最终其实会产生两个op_array。一个是由函数foo编译而来,另一个则是由除去函数foo之外代码编译生成的。同理可以推出,假如一份php脚本其中包含有2个函数和若干语句,则最终会产生3个op_array。也就是说,每个函数最终都会被编译成一个对应的op_array。

刚才提到,op_array中有一些字段是和函数息息相关的。比如function_name代表着函数的名称,比如num_args代表了函数的参数个数,比如required_num_args代表了必须的参数个数,比如arg_info代表着函数的参数信息…etc。

下面会继续结合这段代码,来研究foo函数详细的编译过程。

根据上述理论,现在我们重新来分析下第一个foreach:
第1遍循环,由于$v是一个引用,因此$v =
&$arr[0],$v=$v*2相当于$arr[0]*2,因此$arr变成2,2,3
第2遍循环,$v = &$arr[1],$arr变成2,4,3
第3遍循环,$v = &$arr[2],$arr变成2,4,6
随后代码进入了第二个foreach: 第1遍循环,隐含操作$v=$arr[0]被触发,由于此时$v仍然是$arr[2]的引用,即相当于$arr[2]=$arr[0],$arr变成2,4,2
第2遍循环,$v=$arr[1],即$arr[2]=$arr[1],$arr变成2,4,4
第3遍循环,$v=$arr[2],即$arr[2]=$arr[2],$arr变成2,4,4
OK,分析完毕。
如何解决类似问题呢?php手册上有一段提醒: Warning : 数组最后一个元素的 $value 引用在 foreach
循环之后仍会保留。建议使用unset()来将其销毁。
复制代码 代码如下:
$arr = array(1,2,3);
foreach($arr as $k => &$v) {
    $v = $v * 2;
}
unset($v);
foreach($arr as $k => $v) {
    echo “$k”, ” => “, “$v”;
}
// 输出 0=>2  1=>4  2=>6

函数getArticles根据不同的条件返回不同类型的值,有bool、int、还有数组,正常情况这类函数是希望返回数组,然后拿数组去做一些其他操作,

1、语法定义

从zend_language_parser.y文件中可以看出,函数的语法分析大致涉及如下几个推导式:

top_statement:
  statement        { zend_verify_namespace; }
 | function_declaration_statement { zend_verify_namespace; zend_do_early_binding; }
 | class_declaration_statement  { zend_verify_namespace; zend_do_early_binding; }
    ...

function_declaration_statement:unticked_function_declaration_statement{ DO_TICKS(); };unticked_function_declaration_statement:function is_reference T_STRING { zend_do_begin_function_declaration(&$1, &$3, 0, $2.op_type, NULL TSRMLS_CC); }'(' parameter_list ')' '{' inner_statement_list '}' { zend_do_end_function_declaration(&$1 TSRMLS_CC); };

is_reference:
  /* empty */ { $$.op_type = ZEND_RETURN_VAL; }
 | '&'   { $$.op_type = ZEND_RETURN_REF; }
;

parameter_list:
  non_empty_parameter_list
 | /* empty */
;non_empty_parameter_list:optional_class_type T_VARIABLE{ znode tmp;  fetch_simple_variable(&tmp, &$2, 0 TSRMLS_CC); $$.op_type = IS_CONST; Z_LVAL($$.u.constant)=1; Z_TYPE($$.u.constant)=IS_LONG; INIT_PZVAL(&$$.u.constant); zend_do_receive_arg(ZEND_RECV, &tmp, &$$, NULL, &$1, &$2, 0 TSRMLS_CC); }|optional_class_type '&' T_VARIABLE{ znode tmp;  fetch_simple_variable(&tmp, &$3, 0 TSRMLS_CC); $$.op_type = IS_CONST; Z_LVAL($$.u.constant)=1; Z_TYPE($$.u.constant)=IS_LONG; INIT_PZVAL(&$$.u.constant); zend_do_receive_arg(ZEND_RECV, &tmp, &$$, NULL, &$1, &$3, 1 TSRMLS_CC); }|optional_class_type '&' T_VARIABLE '=' static_scalar{ znode tmp;  fetch_simple_variable(&tmp, &$3, 0 TSRMLS_CC); $$.op_type = IS_CONST; Z_LVAL($$.u.constant)=1; Z_TYPE($$.u.constant)=IS_LONG; INIT_PZVAL(&$$.u.constant); zend_do_receive_arg(ZEND_RECV_INIT, &tmp, &$$, &$5, &$1, &$3, 1 TSRMLS_CC); }|optional_class_type T_VARIABLE '=' static_scalar{ znode tmp;  fetch_simple_variable(&tmp, &$2, 0 TSRMLS_CC); $$.op_type = IS_CONST; Z_LVAL($$.u.constant)=1; Z_TYPE($$.u.constant)=IS_LONG; INIT_PZVAL(&$$.u.constant); zend_do_receive_arg(ZEND_RECV_INIT, &tmp, &$$, &$4, &$1, &$2, 0 TSRMLS_CC); }|non_empty_parameter_list ',' optional_class_type T_VARIABLE              { znode tmp;  fetch_simple_variable(&tmp, &$4, 0 TSRMLS_CC); $$=$1; Z_LVAL($$.u.constant)++; zend_do_receive_arg(ZEND_RECV, &tmp, &$$, NULL, &$3, &$4, 0 TSRMLS_CC); }|non_empty_parameter_list ',' optional_class_type '&' T_VARIABLE            { znode tmp;  fetch_simple_variable(&tmp, &$5, 0 TSRMLS_CC); $$=$1; Z_LVAL($$.u.constant)++; zend_do_receive_arg(ZEND_RECV, &tmp, &$$, NULL, &$3, &$5, 1 TSRMLS_CC); }|non_empty_parameter_list ',' optional_class_type '&' T_VARIABLE '=' static_scalar { znode tmp;  fetch_simple_variable(&tmp, &$5, 0 TSRMLS_CC); $$=$1; Z_LVAL($$.u.constant)++; zend_do_receive_arg(ZEND_RECV_INIT, &tmp, &$$, &$7, &$3, &$5, 1 TSRMLS_CC); }|non_empty_parameter_list ',' optional_class_type T_VARIABLE '=' static_scalar    { znode tmp;  fetch_simple_variable(&tmp, &$4, 0 TSRMLS_CC); $$=$1; Z_LVAL($$.u.constant)++; zend_do_receive_arg(ZEND_RECV_INIT, &tmp, &$$, &$6, &$3, &$4, 0 TSRMLS_CC); };

这里并没有截取完整,主要是缺少函数体内语句的语法分析,但已经足够我们弄清楚编译过程中的一些细节。

函数体内的语句,其对应的语法为inner_statement_list。inner_statement_list和函数体之外一般的语句并无二致,可以简单当成普通的语句来编译。

最重要的是看下unticked_function_declaration_statement,它定义了函数语法的骨架,同时还可以看出,函数编译中会执行zend_do_begin_function_declaration以及zend_do_end_function_declaration。这两步分别对应着下文提到的开始编译和结束编译。我们先来看zend_do_begin_function_declaration。

从这个问题中我们可以看出,引用很有可能会伴随副作用。如果不希望无意识的修改导致数组内容变更,最好及时unset掉这些引用。
问题2: 复制代码 代码如下:
$arr = array(‘a’,’b’,’c’);
foreach($arr as $k => $v) {
    echo key($arr), “=>”, current($arr);
}
// 打印 1=>b 1=>b 1=>b

可因为函数返回值类型不固定,调用时就很可能产生各种预想不到的坑,

2、开始编译

当zend
vm的语法分析器遇到一段函数声明时,会尝试开始编译函数,这是通过执行zend_do_begin_function_declaration来完成的。

有两点:

1,函数是否返回引用,通过is_reference判断。可以看到在对is_reference进行语法分析时,可能会将op_type赋予ZEND_RETURN_VAL或ZEND_RETURN_REF。根据我们文章开始给出的php代码示例,函数foo并不返回引用,因此这里$2.op_type为ZEND_RETURN_VAL。

话说由function & func_name() { …
}这种形式来决定是否返回引用,已经很古老了,还是在CI框架中见过,现在很少这么写。但是php做了兼容,所以即使采用这种很陈旧的语法,也能够被正确识别。

2,zend_do_begin_function_declaration接受的第一个参数,是对function字面进行词法分析生成的znode。这个znode被使用得非常巧妙,因为在编译函数时,zend
vm必须将CG(active_op_array)切换成函数自己的op_array,以便于存储函数的编译结果,当函数编译完成之后,zend
vm又需要将将CG(active_op_array)恢复成函数体外层的op_array。利用该znode保存函数体外的op_array,可以很方便的在函数编译结束时进行CG(active_op_array)恢复,具体后面会讲到。

研究下zend_do_begin_function_declaration的实现,比较长,我们分段来看:

// 声明函数会变编译成的op_arrayzend_op_array op_array;// 函数名、长度、起始行数char *name = function_name->u.constant.value.str.val;int name_len = function_name->u.constant.value.str.len;int function_begin_line = function_token->u.opline_num;zend_uint fn_flags;char *lcname;zend_bool orig_interactive;ALLOCA_FLAGif (is_method) {    ...} else {    fn_flags = 0;}// 对函数来说,fn_flags没用,对方法来说,fn_flags指定了方法的修饰符if ((fn_flags & ZEND_ACC_STATIC) && (fn_flags & ZEND_ACC_ABSTRACT) && !(CG(active_class_entry)->ce_flags & ZEND_ACC_INTERFACE)) {    zend_error(E_STRICT, "Static function %s%s%s() should not be abstract", is_method ? CG(active_class_entry)->name : "", is_method ? "::" : "", Z_STRVAL(function_name->u.constant));}

这段代码一开始就印证了我们先前的说法,每个函数都有一份自己的op_array。所以会在开头先声明一个op_array变量。

// 第一个znode参数的妙处,它记录了当前的CG(active_op_array)function_token->u.op_array = CG(active_op_array);lcname = zend_str_tolower_dup(name, name_len);// 对op_array进行初始化,强制op_array.fn_flags会被初始化为0orig_interactive = CG(interactive);CG(interactive) = 0;init_op_array(&op_array, ZEND_USER_FUNCTION, INITIAL_OP_ARRAY_SIZE TSRMLS_CC);CG(interactive) = orig_interactive;// 对op_array的一些设置op_array.function_name = name;op_array.return_reference = return_reference;op_array.fn_flags |= fn_flags;op_array.pass_rest_by_reference = 0;op_array.scope = is_method ? CG(active_class_entry):NULL;op_array.prototype = NULL;op_array.line_start = zend_get_compiled_lineno;

function_token便是对function字面进行词法分析而生成的znode。这段代码一开始,就让它保存当前的CG(active_op_array),即函数体之外的op_array。保存好CG(active_op_array)之后,便会开始对函数自己的op_array进行初始化。

这个问题更加诡异。按照手册的说法,key和current分别是取数组中当前元素的的键值。
那为何key($arr)一直是1,current($arr)一直是b呢?
先用vld查看编译之后的opcode:奥门新浦京官方网站 1

因此我就想,既然不能规范,那直接强制好了。

op_array.fn_flags是个多功能字段,还记得上一篇中提到的交互式么,如果php以交互式打开,则op_array.fn_flags会被初始化为ZEND_ACC_INTERACTIVE,否则会被初始化为0。这里在init_op_array之前设置CG(interactive)

0,便是确保op_array.fn_flags初始化为0。随后会进一步执行op_array.fn_flags
|=
fn_flags,如果是在方法中,则op_array.fn_flags含义为static、abstract、final等修饰符,对函数来讲,op_array.fn_flags依然是0。

zend_op *opline = get_next_op(CG(active_op_array) TSRMLS_CC);// 如果处于命名空间,则函数名还需要加上命名空间if (CG(current_namespace)) {    /* Prefix function name with current namespace name */    znode tmp;    tmp.u.constant = *CG(current_namespace);    zval_copy_ctor(&tmp.u.constant);    zend_do_build_namespace_name(&tmp, &tmp, function_name TSRMLS_CC);    op_array.function_name = Z_STRVAL(tmp.u.constant);    efree;    name_len = Z_STRLEN(tmp.u.constant);    lcname = zend_str_tolower_dup(Z_STRVAL(tmp.u.constant), name_len);}// 设置oplineopline->opcode = ZEND_DECLARE_FUNCTION;// 第一个操作数opline->op1.op_type = IS_CONST;build_runtime_defined_function_key(&opline->op1.u.constant, lcname, name_len TSRMLS_CC);// 第二个操作数opline->op2.op_type = IS_CONST;opline->op2.u.constant.type = IS_STRING;opline->op2.u.constant.value.str.val = lcname;opline->op2.u.constant.value.str.len = name_len;Z_SET_REFCOUNT(opline->op2.u.constant, 1);opline->extended_value = ZEND_DECLARE_FUNCTION;// 切换CG(active_op_array)成函数自己的op_arrayzend_hash_update(CG(function_table), opline->op1.u.constant.value.str.val, opline->op1.u.constant.value.str.len, &op_array, sizeof(zend_op_array), (void **) &CG(active_op_array));

上面这段代码很关键。有几点要说明的:

1,如果函数是处于命名空间中,则其名称会被扩展成命名空间函数名。比如:

<?phpnamespace MyProject;function foo($arg1, $arg2 = 100){    print($arg1);}

则会将函数名改为MyProjectfoo。扩展工作由zend_do_build_namespace_name来完成。

2,build_runtime_defined_function_key会生成一个“key”。除了用到函数名称之外,还用到了函数所在文件路径、代码在内存中的地址等等。具体的实现可以自行阅读。将函数放进CG(function_table)时,用的键便是这个“key”。

以我的机器为例,上述函数foo生成的key为:

00foo/home/work/foo.php00ABEBBF

从这个key中能直观的看出函数名,所在文件路径这两个信息。

3,代码中的op_line获取时,尚未发生CG(active_op_array)的切换。也就是说,op_line依然是外层op_array的一条指令。该指令具体为ZEND_DECLARE_FUNCTION,有两个操作数,第一个操作数保存了第二点中提到的“key”,第二个操作数则保存了形如”myprojectfoo”这样的函数名。

4,这段代码的最后,将函数自身对应的op_array存放进了CG(function_table),同时,完成了CG(active_op_array)的切换。从这条语句开始,CG(active_op_array)便开始指向函数自己的op_array,而不再是函数体外层的op_array了。

继续来看zend_do_begin_function_declaration的最后一段:

// 需要debuginfo,则函数体内的第一条zend_op,为ZEND_EXT_NOPif (CG(compiler_options) & ZEND_COMPILE_EXTENDED_INFO) {    zend_op *opline = get_next_op(CG(active_op_array) TSRMLS_CC);    opline->opcode = ZEND_EXT_NOP;    opline->lineno = function_begin_line;    SET_UNUSED(opline->op1);    SET_UNUSED(opline->op2);}// 控制switch和foreach内声明的函数{    /* Push a seperator to the switch and foreach stacks */    zend_switch_entry switch_entry;    switch_entry.cond.op_type = IS_UNUSED;    switch_entry.default_case = 0;    switch_entry.control_var = 0;    zend_stack_push(&CG(switch_cond_stack), (void *) &switch_entry, sizeof(switch_entry));    {        /* Foreach stack separator */        zend_op dummy_opline;        dummy_opline.result.op_type = IS_UNUSED;        dummy_opline.op1.op_type = IS_UNUSED;        zend_stack_push(&, (void *) &dummy_opline, sizeof;    }}// 保存函数的注释语句if (CG(doc_comment)) {    CG(active_op_array)->doc_comment = CG(doc_comment);    CG(active_op_array)->doc_comment_len = CG(doc_comment_len);    CG(doc_comment) = NULL;    CG(doc_comment_len) = 0;}// 作用和上面switch,foreach是一样的,函数体内的语句并不属于函数体外的labelzend_stack_push(&CG(labels_stack), (void *) &CG, sizeof(HashTable*));CG = NULL;

可能初学者会对CG(switch_cond_stack),CG(foreach_copy_stack),CG(labels_stack)等字段有疑惑。其实也很好理解。以CG(labels_stack)为例,由于进入函数体内之后,op_array发生了切换,外层的CG(active_op_array)被保存到function
znode的u.op_array中(如果记不清楚了回头看上文:-))。因此函数外层已经被parse出的一些label也需要被保存下来,用的正是CG(labels_stack)来保存。当函数体完成编译之后,zend
vm可以从CG(labels_stack)中恢复出原先的label。举例来说,

<?phplabel1:function foo($arg1){    print($arg1);    goto label2;    label2:    exit;}$bar = 'hello php';foo($bar);

解释器在进入zend_do_begin_function_declaration时,CG中保存的是“label1”。当解释器开始编译函数foo,则需要将“label1”保存到CG(labels_stack)中,同时清空CG。因为在编译foo的过程中,CG会保存“labe2”。当foo编译完成,会利用CG(labels_stack)来恢复CG,则CG再次变成“label1”。

至此,整个zend_do_begin_function_declaration过程已经全部分析完成。最重要的是,一旦完成zend_do_begin_function_declaration,CG(active_op_array)就指向了函数自身对应的op_array。同时,也利用生成的“key”在CG(function_table)中替函数占了一个位。

我们从第3行的ASSIGN指令看起,它代表将array(‘a’,’b’,’c’)赋值给$arr。
由于$arr为CV,array(‘a’,’b’,’c’)为TMP,因此ASSIGN指令找到实际执行的函数为ZEND_ASSIGN_SPEC_CV_TMP_HANDLER。这里需要特别指出,CV是PHP5.1之后才增加的一种变量cache,它采用数组的形式来保存zval**,被cache住的变量再次使用时无需去查找active符号表,而是直接去CV数组中获取,由于数组访问速度远超hash表,因而可以提高效率。
复制代码 代码如下:
static int ZEND_FASTCALL 
ZEND_ASSIGN_SPEC_CV_TMP_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
    zend_op *opline = EX(opline);
    zend_free_op free_op2;
    zval *value = _get_zval_ptr_tmp(&opline->op2, EX(Ts),
&free_op2 TSRMLS_CC);

函数/方法返回值可以强制类型,如 图

3、编译参数列表

函数可以定义为不接受任何参数,对于参数列表为空的情况,其实不做任何处理。我们前文的例子foo函数,接受了一个参数$arg1,我们下面还是分析有参数的情况。

根据语法推导式non_empty_parameter_list的定义,参数列表一共有8种,前4种对应的是一个参数,后4种对应多个参数。我们只关心前4种,后4种编译的过程,仅仅是重复前4种的步骤而已。

optional_class_type T_VARIABLE{ znode tmp;  fetch_simple_variable(&tmp, &$2, 0 TSRMLS_CC); $$.op_type = IS_CONST; Z_LVAL($$.u.constant)=1; Z_TYPE($$.u.constant)=IS_LONG; INIT_PZVAL(&$$.u.constant); zend_do_receive_arg(ZEND_RECV, &tmp, &$$, NULL, &$1, &$2, 0 TSRMLS_CC); }optional_class_type '&' T_VARIABLE{ znode tmp;  fetch_simple_variable(&tmp, &$3, 0 TSRMLS_CC); $$.op_type = IS_CONST; Z_LVAL($$.u.constant)=1; Z_TYPE($$.u.constant)=IS_LONG; INIT_PZVAL(&$$.u.constant); zend_do_receive_arg(ZEND_RECV, &tmp, &$$, NULL, &$1, &$3, 1 TSRMLS_CC); }optional_class_type '&' T_VARIABLE '=' static_scalar{ znode tmp;  fetch_simple_variable(&tmp, &$3, 0 TSRMLS_CC); $$.op_type = IS_CONST; Z_LVAL($$.u.constant)=1; Z_TYPE($$.u.constant)=IS_LONG; INIT_PZVAL(&$$.u.constant); zend_do_receive_arg(ZEND_RECV_INIT, &tmp, &$$, &$5, &$1, &$3, 1 TSRMLS_CC); }optional_class_type T_VARIABLE '=' static_scalar{ znode tmp;  fetch_simple_variable(&tmp, &$2, 0 TSRMLS_CC); $$.op_type = IS_CONST; Z_LVAL($$.u.constant)=1; Z_TYPE($$.u.constant)=IS_LONG; INIT_PZVAL(&$$.u.constant); zend_do_receive_arg(ZEND_RECV_INIT, &tmp, &$$, &$4, &$1, &$2, 0 TSRMLS_CC); }

前4种情况,具体又可以分为2类,1类没有默认值,区别只在于参数的传递是否采用引用,而另1类,都有默认值“static_scalar”。

实际上区别并不大,它们的语法分析的处理过程也几乎一致。都是先调用fetch_simple_variable,再执行zend_do_receive_arg。有没有默认值,区别也仅仅在于zend_do_receive_arg的参数,会不会将默认值传递进去。先来看fetch_simple_variable。

    // CV数组中创建出$arr**指针
    zval **variable_ptr_ptr =
_get_zval_ptr_ptr_cv(&opline->op1, EX(Ts), BP_VAR_W
TSRMLS_CC);
    if (IS_CV == IS_VAR && !variable_ptr_ptr) {
        ……
    }
    else {
        // 将array赋值给$arr
         value = zend_assign_to_variable(variable_ptr_ptr, value, 1
TSRMLS_CC);
        if (!RETURN_VALUE_UNUSED(&opline->result)) {
            AI_SET_PTR(EX_T(opline->result.u.var).var, value);
            PZVAL_LOCK(value);
        }
    }
    ZEND_VM_NEXT_OPCODE();
}

奥门新浦京官方网站 2

3.1 fetch_simple_variable

fetch_simple_variable是用来获取compiled variables索引的。compiled
variables被视作php的性能提升手段之一,因为它利用数组存储了变量,而并非内核中普遍使用的HashTable。这里可以看出,函数的任何一个参数,均会被编译为compiled
variables,compiled
variables被保存在函数体op_array->vars数组中。虽然根据变量名称去HashTable查询,效率并不低。但显然根据索引去op_array->vars数组中获取变量,会更加高效。

void fetch_simple_variable_ex(znode *result, znode *varname, int bp, zend_uchar op TSRMLS_DC) /* {{{ */{    zend_op opline;    ...    if (varname->op_type == IS_CONST) {        if (Z_TYPE(varname->u.constant) != IS_STRING) {            convert_to_string(&varname->u.constant);        }        if (!zend_is_auto_global(varname->u.constant.value.str.val, varname->u.constant.value.str.len TSRMLS_CC) &&            !(varname->u.constant.value.str.len == (sizeof("this")-1) && !memcmp(varname->u.constant.value.str.val, "this", sizeof("this"))) &&            (CG(active_op_array)->last == 0 || CG(active_op_array)->opcodes[CG(active_op_array)->last-1].opcode != ZEND_BEGIN_SILENCE)) {                        // 节点的类型为IS_CV,表明是compiled variables            result->op_type = IS_CV;                        // 用u.var来记录compiled variables在CG(active_op_array)->vars中的索引            result->u.var = lookup_cv(CG(active_op_array), varname->u.constant.value.str.val, varname->u.constant.value.str.len);            result->u.EA.type = 0;            varname->u.constant.value.str.val = CG(active_op_array)->vars[result->u.var].name;            return;        }    }    ...}

这里不做详细的分析了。当fetch_simple_variable获取索引之后,znode中就不必再保存变量的名称,取而代之的是变量在vars数组中的索引,即znode->u.var,其类型为int。fetch_simple_variable完成,会进入zend_do_receive_arg。

ASSIGN指令完成之后,CV数组中被加入zval**指针,指针指向实际的array,这表示$arr已经被CV缓存了起来。奥门新浦京官方网站 3

支持四种强制类型限制:int、array、bool、object,当返回值与函数声明中的类型不匹配时,抛出warning,本来想抛出error,但是觉得

3.2 zend_do_receive_arg

zend_do_receive_arg目的是生成一条zend
op指令,可以称作RECV。

一般而言,除非函数不存在参数,否则RECV是函数的第一条指令(这里表述不准,有extend
info时也不是第一条)。该指令的opcode可能为ZEND_RECV或者ZEND_RECV_INIT,取决于是否有默认值。如果参数没有默认值,指令等于ZEND_RECV,有默认值,则为ZEND_RECV_INIT。zend_do_receive_arg的第二个参数,就是上面提到的compiled
variables节点。

分析下zend_do_receive_arg的源码,也是分几段来看:

zend_op *opline;zend_arg_info *cur_arg_info;// class_type主要用于限制函数参数的类型if (class_type->op_type == IS_CONST && Z_TYPE(class_type->u.constant) == IS_STRING && Z_STRLEN(class_type->u.constant) == 0) {    /* Usage of namespace as class name not in namespace */    zval_dtor(&class_type->u.constant);    zend_error(E_COMPILE_ERROR, "Cannot use 'namespace' as a class name");    return;}// 对静态方法来说,参数不能为thisif (var->op_type == IS_CV && var->u.var == CG(active_op_array)->this_var && (CG(active_op_array)->fn_flags & ZEND_ACC_STATIC) == 0) {    zend_error(E_COMPILE_ERROR, "Cannot re-assign $this");} else if (var->op_type == IS_VAR && CG(active_op_array)->scope && ((CG(active_op_array)->fn_flags & ZEND_ACC_STATIC) == 0) && (Z_TYPE(varname->u.constant) == IS_STRING) && (Z_STRLEN(varname->u.constant) == sizeof("this")-1) && (memcmp(Z_STRVAL(varname->u.constant), "this", sizeof("this")) == 0)) {    zend_error(E_COMPILE_ERROR, "Cannot re-assign $this");}// CG(active_op_array)此时已经是函数体的op_array了,这里拿一条指令opline = get_next_op(CG(active_op_array) TSRMLS_CC);CG(active_op_array)->num_args++;opline->opcode = op;opline->result = *var;// op1节点表明是第几个参数opline->op1 = *offset;// op2节点可能为初始值,也可能为UNUSEDif (op == ZEND_RECV_INIT) {    opline->op2 = *initialization;} else {    CG(active_op_array)->required_num_args = CG(active_op_array)->num_args;    SET_UNUSED(opline->op2);}

上面这段代码,首先通过get_next_op(CG(active_op_array)
TSRMLS_CC)一句获取了opline,opline是未被使用的一条zend_op指令。紧接着,会对opline的各个字段进行设置。opline->op1表明这是第几个参数,opline->op2可能为初始值,也可能被设置为UNUSED。

如果一个参数有默认值,那么在调用函数时,其实是可以不用传递该参数的。所以,required_num_args不会将这类非必须的参数算进去的。可以看到,在op
== ZEND_RECV_INIT这段逻辑分支中,并没有处理required_num_args。

继续来看:

// 这里采用erealloc进行分配,因为期望最终会形成一个参数信息的数组CG(active_op_array)->arg_info = erealloc(CG(active_op_array)->arg_info, sizeof(zend_arg_info)*(CG(active_op_array)->num_args));// 设置当前的zend_arg_infocur_arg_info = &CG(active_op_array)->arg_info[CG(active_op_array)->num_args-1];cur_arg_info->name = estrndup(varname->u.constant.value.str.val, varname->u.constant.value.str.len);cur_arg_info->name_len = varname->u.constant.value.str.len;cur_arg_info->array_type_hint = 0;cur_arg_info->allow_null = 1;cur_arg_info->pass_by_reference = pass_by_reference;cur_arg_info->class_name = NULL;cur_arg_info->class_name_len = 0;// 如果需要对参数做类型限定if (class_type->op_type != IS_UNUSED) {    cur_arg_info->allow_null = 0;        // 限定为类    if (class_type->u.constant.type == IS_STRING) {        if (ZEND_FETCH_CLASS_DEFAULT == zend_get_class_fetch_type(Z_STRVAL(class_type->u.constant), Z_STRLEN(class_type->u.constant))) {            zend_resolve_class_name(class_type, &opline->extended_value, 1 TSRMLS_CC);        }        cur_arg_info->class_name = class_type->u.constant.value.str.val;        cur_arg_info->class_name_len = class_type->u.constant.value.str.len;                // 如果限定为类,则参数的默认值只能为NULL        if (op == ZEND_RECV_INIT) {            if (Z_TYPE(initialization->u.constant) == IS_NULL || (Z_TYPE(initialization->u.constant) == IS_CONSTANT && !strcasecmp(Z_STRVAL(initialization->u.constant), "NULL"))) {                cur_arg_info->allow_null = 1;            } else {                zend_error(E_COMPILE_ERROR, "Default value for parameters with a class type hint can only be NULL");            }        }    }    // 限定为数组    else {        // 将array_type_hint设置为1        cur_arg_info->array_type_hint = 1;        cur_arg_info->class_name = NULL;        cur_arg_info->class_name_len = 0;                // 如果限定为数组,则参数的默认值只能为数组或NULL        if (op == ZEND_RECV_INIT) {            if (Z_TYPE(initialization->u.constant) == IS_NULL || (Z_TYPE(initialization->u.constant) == IS_CONSTANT && !strcasecmp(Z_STRVAL(initialization->u.constant), "NULL"))) {                cur_arg_info->allow_null = 1;            } else if (Z_TYPE(initialization->u.constant) != IS_ARRAY && Z_TYPE(initialization->u.constant) != IS_CONSTANT_ARRAY) {                zend_error(E_COMPILE_ERROR, "Default value for parameters with array type hint can only be an array or NULL");            }        }    }}opline->result.u.EA.type |= EXT_TYPE_UNUSED;

这部分代码写的很清晰。注意,对于限定为数组的情况,class_type的op_type会被设置为IS_CONST,而u.constant.type会被设置为IS_NULL:

optional_class_type:/* empty */{ $$.op_type = IS_UNUSED; }|fully_qualified_class_name{ $$ = $1; }|T_ARRAY{ $$.op_type = IS_CONST; Z_TYPE($$.u.constant)=IS_NULL;}

因此,zend_do_receive_arg中区分限定为类还是数组,是利用class_type->u.constant.type
== IS_STRING来判断的。如果类型限定为数组,则cur_arg_info->array_type_hint会被设置为1。

还有另一个地方需要了解,zend_resolve_class_name函数会修正类名。举例来说:

<?phpnamespace A;class B { }function foo(B $arg1, $arg2 = 100){    print($arg1);}

我们期望参数arg1的类型为B,class_type中也保存了B。但是因为位于命名空间A下,所以,zend_resolve_class_name会将class_type中保存的类名B,修正为AB。

OK,到这里,zend_do_receive_arg已经全部分析完。zend
vm在分析函数参数时,每遇见一个参数,便会调用一次zend_do_receive_arg,生成一条RECV指令。因此,函数有几个参数,就会编译出几条RECV指令。

接下来执行数组的循环操作,我们来看FE_RESET指令,它对应的执行函数为ZEND_FE_RESET_SPEC_CV_HANDLER: 复制代码 代码如下:
static int ZEND_FASTCALL 
ZEND_FE_RESET_SPEC_CV_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
    ……
    if (……) {
        ……
    } else {
        // 通过CV数组获取指向array的指针
        array_ptr = _get_zval_ptr_cv(&opline->op1, EX(Ts),
BP_VAR_R TSRMLS_CC);
        ……
    }
    ……
    //
将指向array的指针保存到zend_execute_data->Ts中(Ts用于存放代码执行期的temp_variable)
    AI_SET_PTR(EX_T(opline->result.u.var).var, array_ptr);
    PZVAL_LOCK(array_ptr);
    if (iter) {
        ……
    } else if ((fe_ht = HASH_OF(array_ptr)) != NULL) {
        // 重置数组内部指针
        zend_hash_internal_pointer_reset(fe_ht);
        if (ce) {
            ……
        }
        is_empty = zend_hash_has_more_elements(fe_ht) !=
SUCCESS;

太狠了,只能算是个异常,不能算错误,所以就用warning好了。

4、编译函数体

当编译完参数列表,zend
vm便会进入函数内部了。函数体的编译其实和正常语句的编译一样。zend
vm只需要将函数体内部的php语句,按照正常的statment,进行词法分析、语法分析来处理,最终形成一条条zend_op指令。

来看下语法文件:

unticked_function_declaration_statement:function is_reference T_STRING { zend_do_begin_function_declaration(&$1, &$3, 0, $2.op_type, NULL TSRMLS_CC); }'(' parameter_list ')' '{' inner_statement_list '}' { zend_do_end_function_declaration(&$1 TSRMLS_CC); }
;

函数体内部的语句,表示为inner_statement_list。

inner_statement_list:inner_statement_list  { zend_do_extended_info; } inner_statement { HANDLE_INTERACTIVE(); }|/* empty */;

而inner_statment正是由语句、函数声明、类声明组成的。

inner_statement:statement|function_declaration_statement|class_declaration_statement|T_HALT_COMPILER '' ';'   { zend_error(E_COMPILE_ERROR, "__HALT_COMPILER() can only be used from the outermost scope"); };

inner_statement并非专门用于函数,其他譬如foreach,while循环等有block语句块中,都会被识别为inner_statement。从这里其实还能看到一些有意思的语法,比如说我们可以在函数里声明一个类。inner_statement就不展开叙述了,否则相当于将整个php的语法捋一遍,情况太多了。

        //
设置EX_T(opline->result.u.var).fe.fe_pos用于保存数组内部指针
        zend_hash_get_pointer(fe_ht,
&EX_T(opline->result.u.var).fe.fe_pos);
    } else {
        ……
    }
    ……
}

PHP本身是不支持 int function
这样的语法的,所以要支持,就先要搞定语法解析器,关于语法解析器,可以移步这里>>>查看

5、结束编译

我们最后来看下结束编译的过程。结束函数编译是通过zend_do_end_function_declaration来完成的。

zend_do_end_function_declaration接收的参数function_token,其实就是前面提到过的function字面对应的znode。根据我们在“开始编译”一节所述,function_token中保留了函数体之外的op_array。

char lcname[16];int name_len;zend_do_extended_info;// 返回NULLzend_do_return(NULL, 0 TSRMLS_CC);// 通过op指令设置对应的handler函数pass_two(CG(active_op_array) TSRMLS_CC);// 释放当前函数的CG,并从CG(labels_stack)中还原之前的CGzend_release_labels;if (CG(active_class_entry)) {    // 检查魔术方法的参数是否合法    zend_check_magic_method_implementation(CG(active_class_entry), (zend_function*)CG(active_op_array), E_COMPILE_ERROR TSRMLS_CC);} else {    /* we don't care if the function name is longer, in fact lowercasing only      * the beginning of the name speeds up the check process */    name_len = strlen(CG(active_op_array)->function_name);    zend_str_tolower_copy(lcname, CG(active_op_array)->function_name, MIN(name_len, sizeof-1));    lcname[sizeof-1] = ''; /* zend_str_tolower_copy won't necessarily set the zero byte */        // 检查__autoload函数的参数是否合法    if (name_len == sizeof(ZEND_AUTOLOAD_FUNC_NAME) - 1 && !memcmp(lcname, ZEND_AUTOLOAD_FUNC_NAME, sizeof(ZEND_AUTOLOAD_FUNC_NAME)) && CG(active_op_array)->num_args != 1) {        zend_error(E_COMPILE_ERROR, "%s() must take exactly 1 argument", ZEND_AUTOLOAD_FUNC_NAME);    }        }CG(active_op_array)->line_end = zend_get_compiled_lineno;// 很关键!将CG(active_op_array)还原成函数外层的op_arrayCG(active_op_array) = function_token->u.op_array;/* Pop the switch and foreach seperators */zend_stack_del_top(&CG(switch_cond_stack));zend_stack_del_top(&CG(foreach_copy_stack));

有3处值得注意:

1,zend_do_end_function_declaration中会对CG(active_op_array)进行还原。用的正是function_token->u.op_array。一旦zend_do_end_function_declaration完成,函数的整个编译过程就已经结束了。zend
vm会继续看接下来函数之外的代码,所以需要将CG(active_op_array)切换成原先的。

2,zend_do_return负责在函数最后添加上一条RETURN指令,因为我们传进去的是NULL,所以这条RETURN指令的操作数被强制设置为UNUSED。注意,不管函数本身是否有return语句,最后这条RETURN指令是必然存在的。假如函数有return语句,return语句也会产生一条RETURN指令,所以会导致可能出现多条RETURN指令。举例来说:

function foo()
{    return true;}

编译出来的OP指令最后两条如下:

 RETURN        true RETURN        null

我们可以很明显在最后看到两条RETURN。一条是通过return
true编译出来的。另一条,就是在zend_do_end_function_declaration阶段,强制插入的RETURN。

3,我们刚才讲解的所有步骤中,都只是设置了每条指令的opcode,而并没有设置这条指令具体的handle函数。pass_two会负责遍历每条zend_op指令,根据opcode,以及操作数op1和op2,去查找并且设置对应的handle函数。这项工作,是通过ZEND_VM_SET_OPCODE_HANDLER宏来完成的。

#define ZEND_VM_SET_OPCODE_HANDLER zend_vm_set_opcode_handler

zend_vm_set_opcode_handler的实现很简单:

void zend_init_opcodes_handlers(void){    // 超大的数组,里面存放了所有的handler    static const opcode_handler_t labels[] = {        ZEND_NOP_SPEC_HANDLER,        ZEND_NOP_SPEC_HANDLER,        ZEND_NOP_SPEC_HANDLER,        ZEND_NOP_SPEC_HANDLER,        ZEND_NOP_SPEC_HANDLER,        ZEND_NOP_SPEC_HANDLER,        ...    };    zend_opcode_handlers = (opcode_handler_t*)labels;}static opcode_handler_t zend_vm_get_opcode_handler(zend_uchar opcode, zend_op* op){        static const int zend_vm_decode[] = {            _UNUSED_CODE, /* 0              */            _CONST_CODE,  /* 1 = IS_CONST   */            _TMP_CODE,    /* 2 = IS_TMP_VAR */            _UNUSED_CODE, /* 3              */            _VAR_CODE,    /* 4 = IS_VAR     */            _UNUSED_CODE, /* 5              */            _UNUSED_CODE, /* 6              */            _UNUSED_CODE, /* 7              */            _UNUSED_CODE, /* 8 = IS_UNUSED  */            _UNUSED_CODE, /* 9              */            _UNUSED_CODE, /* 10             */            _UNUSED_CODE, /* 11             */            _UNUSED_CODE, /* 12             */            _UNUSED_CODE, /* 13             */            _UNUSED_CODE, /* 14             */            _UNUSED_CODE, /* 15             */            _CV_CODE      /* 16 = IS_CV     */        };                // 去handler数组里找到对应的处理函数        return zend_opcode_handlers[opcode * 25 + zend_vm_decode[op->op1.op_type] * 5 + zend_vm_decode[op->op2.op_type]];}ZEND_API void zend_vm_set_opcode_handler(zend_op* op){    // 给zend op设置对应的handler函数    op->handler = zend_vm_get_opcode_handler(zend_user_opcodes[op->opcode], op);}

所有的opcode都定义在zend_vm_opcodes.h里,从php5.3-php5.6,大概从150增长到170个opcode。上面可以看到通过opcode查找handler的准确算法:

zend_opcode_handlers[opcode * 25 + zend_vm_decode[op->op1.op_type] * 5 + zend_vm_decode[op->op2.op_type]

不过zend_opcode_handlers数组太大了…找起来很麻烦。

下面回到文章开始的那段php代码,我们将函数foo进行编译,最终得到的指令如下:

奥门新浦京官方网站 4

可以看出,因为foo指接受一个参数,所以这里只有一条RECV指令。

print语句的参数为!0,!0是一个compiled
variables,其实就是参数中的arg1。0代表着索引,回忆一下,函数的op_array有一个数组专门用于保存compiled
variables,0表明arg1位于该数组的开端。

print语句有返回值,所以会存在一个临时变量保存其返回值,即~0。由于我们在函数中并未使用~0,所以随即便会有一条FREE指令对其进行释放。

在函数的最后,是一条RETURN指令。

这里主要将2个重要的指针存入了zend_execute_data->Ts中:
•EX_T(opline->result.u.var).var —- 指向array的指针
•EX_T(opline->result.u.var).fe.fe_pos —-
指向array内部元素的指针
FE_RESET指令执行完毕之后,内存中实际情况如下:

详情,这里就不讲了,

6、绑定

函数编译完成之后,还需要进行的一步是绑定。zend
vm通过zend_do_early_binding来实现绑定。这个名字容易让人产生疑惑,其实只有在涉及到类和方法的时候,才会有早期绑定,与之相对的是延迟绑定,或者叫后期绑定。纯粹函数谈不上这种概念,不过zend_do_early_binding是多功能的,并非仅仅为绑定方法而实现。

来看下zend_do_early_binding:

// 拿到的是最近一条zend op,对于函数来说,就是ZEND_DECLARE_FUNCTIONzend_op *opline = &CG(active_op_array)->opcodes[CG(active_op_array)->last-1];HashTable *table;while (opline->opcode == ZEND_TICKS && opline > CG(active_op_array)->opcodes) {    opline--;}switch (opline->opcode) {    case ZEND_DECLARE_FUNCTION:        // 真正绑定函数        if (do_bind_function(opline, CG(function_table), 1) == FAILURE) {            return;        }        table = CG(function_table);        break;    case ZEND_DECLARE_CLASS:        ...    case ZEND_DECLARE_INHERITED_CLASS:        ...}// op1中保存的是函数的key,这里其从将CG(function_table)中删除zend_hash_del(table, opline->op1.u.constant.value.str.val, opline->op1.u.constant.value.str.len);zval_dtor(&opline->op1.u.constant);zval_dtor(&opline->op2.u.constant);// opline置为NOPMAKE_NOP;

这个函数实现也很简单,主要就是调用了do_bind_function。

ZEND_API int do_bind_function(zend_op *opline, HashTable *function_table, zend_bool compile_time) /* {{{ */{    zend_function *function;    // 找出函数    zend_hash_find(function_table, opline->op1.u.constant.value.str.val, opline->op1.u.constant.value.str.len, (void *) &function);        // 以函数名称作为key,重新加入function_table    if (zend_hash_add(function_table, opline->op2.u.constant.value.str.val, opline->op2.u.constant.value.str.len+1, function, sizeof(zend_function), NULL)==FAILURE) {        int error_level = compile_time ? E_COMPILE_ERROR : E_ERROR;        zend_function *old_function;        // 加入失败,可能发生重复定义了        if (zend_hash_find(function_table, opline->op2.u.constant.value.str.val, opline->op2.u.constant.value.str.len+1, (void *) &old_function)==SUCCESS            && old_function->type == ZEND_USER_FUNCTION            && old_function->op_array.last > 0) {            zend_error(error_level, "Cannot redeclare %s() (previously declared in %s:%d)",                        function->common.function_name, old_function->op_array.filename, old_function->op_array.opcodes[0].lineno);        } else {            zend_error(error_level, "Cannot redeclare %s()", function->common.function_name);        }        return FAILURE;    } else {        (*function->op_array.refcount)++;        function->op_array.static_variables = NULL; /* NULL out the unbound function */        return SUCCESS;    }}

在进入do_bind_function之前,其实CG(function_table)中已经有了函数的op_array。不过用的键并非函数名,而是build_runtime_defined_function_key生成的“key”,这点在前面“开始编译”一节中有过介绍。do_bind_function所做的事情,正是利用这个“key”,将函数查找出来,并且以真正的函数名为键,重新插入到CG(function_table)中。

因此当do_bind_function完成时,function_table中有2个键可以查询到该函数。一个是“key”为索引的,另一个是以函数名为索引的。在zend_do_early_binding的最后,会通过zend_hash_del来删除“key”,从而保证function_table中,该函数只能够以函数名为键值查询到。

奥门新浦京官方网站 5

先修改语法扫描 Zend/zend_language_scanner.l文件

7、总结

这篇其实主要是为了弄清楚,函数如何被编译成op_array。一些关键的步骤如下图:

奥门新浦京官方网站 6

至于函数的调用,又是另外一个话题了。

接下来我们继续查看FE_FETCH,它对应的执行函数为ZEND_FE_FETCH_SPEC_VAR_HANDLER:
复制代码 代码如下:
static int ZEND_FASTCALL 
ZEND_FE_FETCH_SPEC_VAR_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
    zend_op *opline = EX(opline);

增加如下代码:

    // 注意指针是从EX_T(opline->op1.u.var).var.ptr获取的
    zval *array = EX_T(opline->op1.u.var).var.ptr;
    ……

<ST_IN_SCRIPTING>”int” {
return T_FUNCTION_RETURN_INT;
}
<ST_IN_SCRIPTING>”bool” {
return T_FUNCTION_RETURN_OBJECT;
}
<ST_IN_SCRIPTING>”object” {
return T_FUNCTION_RETURN_OBJECT;
}
<ST_IN_SCRIPTING>”resource” {
return T_FUNCTION_RETURN_RESOURCE;
}

    switch (zend_iterator_unwrap(array, &iter TSRMLS_CC)) {
        default:
        case ZEND_ITER_INVALID:
            ……
        case ZEND_ITER_PLAIN_OBJECT: {
            ……
        }
        case ZEND_ITER_PLAIN_ARRAY:
            fe_ht = HASH_OF(array);

意思很简单,扫描器扫描到到关键字
int、bool、object、resource、array时返回相应的T_FUNCTION_*
,这是一个token,

            // 特别注意:
            //
FE_RESET指令中将数组内部元素的指针保存在EX_T(opline->op1.u.var).fe.fe_pos
            // 此处获取该指针
            zend_hash_set_pointer(fe_ht,
&EX_T(opline->op1.u.var).fe.fe_pos);

scanner根据不同的token做不同的处理,token要先在Zend/zend_language_parser.y文件中定义

            // 获取元素的值
            if (zend_hash_get_current_data(fe_ht, (void **)
&value)==FAILURE) {
               
ZEND_VM_JMP(EX(op_array)->opcodes+opline->op2.u.opline_num);
            }
            if (use_key) {
                key_type = zend_hash_get_current_key_ex(fe_ht,
&str_key, &str_key_len, &int_key, 1, NULL);
            }

增加如下代码

            // 数组内部指针移动到下一个元素
            zend_hash_move_forward(fe_ht);

……….
%token T_FUNCTION_RETURN_INT
%token T_FUNCTION_RETURN_BOOL
%token T_FUNCTION_RETURN_STRING
%token T_FUNCTION_RETURN_OBJECT
%token T_FUNCTION_RETURN_RESOURCE
1

然后增加token处理逻辑:

1
function:
T_FUNCTION { $$.u.opline_num = CG(zend_lineno);$$.u.EA.var  = 0;
}
|   T_FUNCTION_RETURN_INT T_FUNCTION {
$$.u.opline_num = CG(zend_lineno);
$$.u.EA.var = IS_LONG;
}
|   T_FUNCTION_RETURN_BOOL T_FUNCTION {
$$.u.opline_num = CG(zend_lineno);
$$.u.EA.var = IS_BOOL;
}
|   T_FUNCTION_RETURN_STRING T_FUNCTION {
$$.u.opline_num = CG(zend_lineno);
$$.u.EA.var = IS_STRING;
}
|   T_FUNCTION_RETURN_OBJECT T_FUNCTION {
$$.u.opline_num = CG(zend_lineno);
$$.u.EA.var = IS_OBJECT;
}
|   T_FUNCTION_RETURN_RESOURCE T_FUNCTION {
$$.u.opline_num = CG(zend_lineno);
$$.u.EA.var = IS_RESOURCE;
}
|   T_ARRAY T_FUNCTION {
$$.u.opline_num = CG(zend_lineno);
$$.u.EA.var = IS_ARRAY;
}

            //
移动之后的指针保存到EX_T(opline->op1.u.var).fe.fe_pos
            zend_hash_get_pointer(fe_ht,
&EX_T(opline->op1.u.var).fe.fe_pos);
            break;
        case ZEND_ITER_OBJECT:
            ……
    }

$$.u.EA.var 存储的是 函数返回类型,最后要拿他来跟返回值类型做匹配,

    ……
}

这样语法解释器就可以处理我们新的php语法了。

根据FE_FETCH的实现,我们大致上明白了foreach($arr as $k =>
$v)所做的事情。它会根据zend_execute_data->Ts的指针去获取数组元素,在获取成功之后,将该指针移动到下一个位置再重新保存。

这还不够,还需要修改函数声明定义的处理逻辑

奥门新浦京官方网站 7

Zend/zend_compile.c ::zend_do_begin_function_declaration

……
zend_op_array op_array;
char *name = function_name->u.constant.value.str.val;
int name_len = function_name->u.constant.value.str.len;
int function_type  = function_token->u.EA.var;
//保存函数类型,在语法解释器中增加的: $$.u.EA.var = IS_LONG;
int function_begin_line = function_token->u.opline_num;
……
op_array.function_name = name;
op_array.fn_type = function_type; //将类型保存到op_array中,
op_array.return_reference = return_reference;
op_array.fn_flags |= fn_flags;
op_array.pass_rest_by_reference = 0;
……….

简单来说,由于第一遍循环中FE_FETCH中已经将数组的内部指针移动到了第二个元素,所以在foreach内部调用key($arr)和current($arr)时,实际上获取的便是1和’b’。
那为何会输出3遍1=>b呢? 我们继续看第9行和第13行的SEND_REF指令,它表示将$arr参数压栈。紧接着一般会使用DO_FCALL指令去调用key和current函数。PHP并非被编译成本地机器码,因此php采用这样的opcode指令去模拟实际CPU和内存的工作方式。
查阅PHP源码中的SEND_REF: 复制代码 代码如下:
static int ZEND_FASTCALL 
ZEND_SEND_REF_SPEC_CV_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
    ……
    // 从CV中获取$arr指针的指针
    varptr_ptr = _get_zval_ptr_ptr_cv(&opline->op1, EX(Ts),
BP_VAR_W TSRMLS_CC);
    ……

PHP是先解析PHP语法生成相应的opcode,将需要的环境、参数信息保存到execute_data全局变量中,最后在通过execute函数逐条执行opcode,

    // 变量分离,此处重新copy了一份array专门用于key函数
    SEPARATE_ZVAL_TO_MAKE_IS_REF(varptr_ptr);
    varptr = *varptr_ptr;
    Z_ADDREF_P(varptr);

所以要做处理就要把函数的类型保存到opcode中:op_array.fn_type =
function_type;

    // 压栈
    zend_vm_stack_push(varptr TSRMLS_CC);
    ZEND_VM_NEXT_OPCODE();
}

op_array是没有fn_type的,要修改op_array的结构,增加zend_uint
fn_type;

上述代码中的SEPARATE_ZVAL_TO_MAKE_IS_REF是一个宏:
复制代码 代码如下:
#define SEPARATE_ZVAL_TO_MAKE_IS_REF(ppzv)   
    if (!PZVAL_IS_REF(*ppzv)) {               
        SEPARATE_ZVAL(ppzv);               
        Z_SET_ISREF_PP((ppzv));               
    }

(关于opcode你可以想象一下
从c转为汇编,我博客中也有相关文章,可以参考一下)

SEPARATE_ZVAL_TO_MAKE_IS_REF的主要作用为,如果变量不是一个引用,则在内存中copy出一份新的。本例中它将array(‘a’,’b’,’c’)复制了一份。因此变量分离之后的内存为:奥门新浦京官方网站 8
注意,变量分离完成之后,CV数组中的指针指向了新copy出来的数据,而通过zend_execute_data->Ts中的指针则依然可以获取旧的数据。
接下来的循环就不一一赘述了,结合上图来说: •foreach结构使用的是下方蓝色的array,会依次遍历a,b,c
•key、current使用的是上方黄色的array,它的内部指针永远指向b
至此我们明白了为何key和current一直返回array的第二个元素,由于没有外部代码作用于copy出来的array,它的内部指针便永远不会移动。
问题3: 复制代码 代码如下:
$arr = array(‘a’,’b’,’c’);
foreach($arr as $k => &$v) {
    echo key($arr), ‘=>’, current($arr);
}// 打印 1=>b 2=>c =>

最后要修改opcode的毁掉函数,函数的返回 return 会生成token
T_RETURN,T_RETURN会根据返回的类型调用不同的calback函数:

本题与问题2仅有一点区别:本题中的foreach使用了引用。用VLD查看本题,发现与问题2代码编译出来的opcode一样。因此我们采用问题2的跟踪方法,逐步查看opcode对应的实现。
首先foreach会调用FE_RESET: 复制代码 代码如下:
static int ZEND_FASTCALL 
ZEND_FE_RESET_SPEC_CV_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
    ……
    if (opline->extended_value & ZEND_FE_RESET_VARIABLE) {
        // 从CV中获取变量
        array_ptr_ptr = _get_zval_ptr_ptr_cv(&opline->op1,
EX(Ts), BP_VAR_R TSRMLS_CC);
        if (array_ptr_ptr == NULL || array_ptr_ptr ==
&EG(uninitialized_zval_ptr)) {
            ……
        }
        else if (Z_TYPE_PP(array_ptr_ptr) == IS_OBJECT) {
            ……
        }
        else {
            // 针对遍历array的情况
            if (Z_TYPE_PP(array_ptr_ptr) == IS_ARRAY) {
                SEPARATE_ZVAL_IF_NOT_REF(array_ptr_ptr);
                if (opline->extended_value & ZEND_FE_FETCH_BYREF)
{
                    // 将保存array的zval设置为is_ref
                    Z_SET_ISREF_PP(array_ptr_ptr);
                }
            }
            array_ptr = *array_ptr_ptr;
            Z_ADDREF_P(array_ptr);
        }
    } else {
        ……
    }
    ……
}

ZEND_RETURN_SPEC_CONST_HANDLER
ZEND_RETURN_SPEC_TMP_HANDLER
ZEND_RETURN_SPEC_VAR_HANDLER

问题2中已经分析了一部分FE_RESET的实现。这里需要特别注意,本例foreach获取值采用了引用,因此在执行的时候FE_RESET中会进入与上题不同的另一个分支。
最终,FE_RESET会将array的is_ref设置为true,此时内存中只有一份array的数据。
接下来分析SEND_REF: 复制代码 代码如下:
static int ZEND_FASTCALL 
ZEND_SEND_REF_SPEC_CV_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
    ……
    // 从CV中获取$arr指针的指针
    varptr_ptr = _get_zval_ptr_ptr_cv(&opline->op1, EX(Ts),
BP_VAR_W TSRMLS_CC);
    ……

它有三个callback,如果返回值是一个 const类型的数据,则
ZEND_RETURN_SPEC_CONST_HANDLER
返回值是临时数据,如 : return 1,则ZEND_RETURN_SPEC_TMP_HANDLER
返回值是一个变量,如 : return $a,则ZEND_RETURN_SPEC_VAR_HANDLER

    //
变量分离,由于此时CV中的变量本身就是一个引用,此处不会copy一份新的array
    SEPARATE_ZVAL_TO_MAKE_IS_REF(varptr_ptr);
    varptr = *varptr_ptr;
    Z_ADDREF_P(varptr);

所以要在这三个callback函数中增加处理逻辑:

    // 压栈
    zend_vm_stack_push(varptr TSRMLS_CC);
    ZEND_VM_NEXT_OPCODE();
}

在callback函数return之前增加如下代码

宏SEPARATE_ZVAL_TO_MAKE_IS_REF仅仅分离is_ref=false的变量。由于之前array已经被设置了is_ref=true,因此它不会被拷贝一份副本。换句话说,此时内存中依然只有一份array数据。

if((EG(active_op_array)->fn_type > 0) &&
Z_TYPE_P(retval_ptr) != EG(active_op_array)->fn_type){
php_error_docref0(NULL TSRMLS_DC,E_WARNING, “function name %s
return a wrong type.”, EG(active_op_array)->function_name );
}

奥门新浦京官方网站 9

fn_type 去跟 返回值的类型作比较,如果没有匹配到,就会抛出这个warning。

上图解释了前2次循环为何会输出1=>b
2=>C。在第3次循环FE_FETCH的时候,将指针继续向前移动。
复制代码 代码如下:
ZEND_API int zend_hash_move_forward_ex(HashTable *ht, HashPosition
*pos)
{
    HashPosition *current = pos ? pos : &ht->pInternalPointer;
    IS_CONSISTENT(ht);
    if (*current) {
        *current = (*current)->pListNext;
        return SUCCESS;
    } else
        return FAILURE;
}

我已经打了补丁,目前只支持php5.3版本,有需要的可以拿去玩一玩。

由于此时内部指针已经指向了数组的最后一个元素,因此再向前移动会指向NULL。将内部指针指向NULL之后,我们再对数组调用key和current,则分别会返回NULL和false,表示调用失败,此时是echo不出字符的。
 问题4: 复制代码 代码如下:
$arr = array(1, 2, 3);
$tmp = $arr;
foreach($tmp as $k => &$v){
    $v *= 2;
}
var_dump($arr, $tmp); // 打印什么?

不清楚为什么官方不支持此语法,我觉得还是挺有必要的。

该题与foreach关系不大,不过既然涉及到了foreach,就一起拿来讨论吧:)
代码里首先创建了数组$arr,随后将该数组赋给了$tmp,在接下来的foreach循环中,对$v进行修改会作用于数组$tmp上,但是却并不作用到$arr。
为什么呢? 这是由于在php中,赋值运算是将一个变量的值拷贝到另一个变量中,因此修改其中一个,并不会影响到另一个。
题外话:这并不适用于object类型,从PHP5起,对象的便总是默认通过引用进行赋值,举例来说:
复制代码 代码如下:
class A{
    public $foo = 1;
}
$a1 = $a2 = new A;
$a1->foo=100;
echo $a2->foo; // 输出100,$a1与$a2其实为同一个对象的引用

下载补丁:php-syntax.patch

回到题目中的代码,现在我们可以确定$tmp=$arr其实是值拷贝,整个$arr数组会被再复制一份给$tmp。理论上讲,赋值语句执行完毕之后,内存中会有2份一样的数组。
也许有同学会疑问,如果数组很大,岂不是这种操作会很慢?
幸好php有更聪明的处理办法。实际上,当$tmp=$arr执行之后,内存中依然只有一份array。查看php源码中的zend_assign_to_variable实现(摘自php5.3.26):
复制代码 代码如下:
static inline zval* zend_assign_to_variable(zval
**variable_ptr_ptr, zval *value, int is_tmp_var TSRMLS_DC)
{
    zval *variable_ptr = *variable_ptr_ptr;
    zval garbage;
    ……
  // 左值为object类型
    if (Z_TYPE_P(variable_ptr) == IS_OBJECT &&
Z_OBJ_HANDLER_P(variable_ptr, set)) {
        ……
    }
    // 左值为引用的情况
    if (PZVAL_IS_REF(variable_ptr)) {
        ……
    } else {
        // 左值refcount__gc=1的情况
        if (Z_DELREF_P(variable_ptr)==0) {
            ……
        } else {
            GC_ZVAL_CHECK_POSSIBLE_ROOT(*variable_ptr_ptr);
            // 非临时变量
            if (!is_tmp_var) {
                if (PZVAL_IS_REF(value) && Z_REFCOUNT_P(value) >
0) {
                    ALLOC_ZVAL(variable_ptr);
                    *variable_ptr_ptr = variable_ptr;
                    *variable_ptr = *value;
                    Z_SET_REFCOUNT_P(variable_ptr, 1);
                    zval_copy_ctor(variable_ptr);
                } else {
                    // $tmp=$arr会运行到这里,
                    //
value为指向$arr里实际array数据的指针,variable_ptr_ptr为$tmp里指向数据指针的指针
                    // 仅仅是复制指针,并没有真正拷贝实际的数组
                    *variable_ptr_ptr = value;
                    //
value的refcount__gc值+1,本例中refcount__gc为1,Z_ADDREF_P之后为2
                    Z_ADDREF_P(value);
                }
            } else {
                ……
            }
        }
        Z_UNSET_ISREF_PP(variable_ptr_ptr);
    }
    return *variable_ptr_ptr;
}

可见$tmp =
$arr的本质就是将array的指针进行复制,然后将array的refcount自动加1.用图表达出此时的内存,依然只有一份array数组:
奥门新浦京官方网站 10
既然只有一份array,那foreach循环中修改$tmp的时候,为何$arr没有跟着改变?
继续看PHP源码中的ZEND_FE_RESET_SPEC_CV_HANDLER函数,这是一个OPCODE
HANDLER,它对应的OPCODE为FE_RESET。该函数负责在foreach开始之前,将数组的内部指针指向其第一个元素。
复制代码 代码如下:
static int ZEND_FASTCALL 
ZEND_FE_RESET_SPEC_CV_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
    zend_op *opline = EX(opline);
    zval *array_ptr, **array_ptr_ptr;
    HashTable *fe_ht;
    zend_object_iterator *iter = NULL;
    zend_class_entry *ce = NULL;
    zend_bool is_empty = 0;
    // 对变量进行FE_RESET
    if (opline->extended_value & ZEND_FE_RESET_VARIABLE) {
        array_ptr_ptr = _get_zval_ptr_ptr_cv(&opline->op1,
EX(Ts), BP_VAR_R TSRMLS_CC);
        if (array_ptr_ptr == NULL || array_ptr_ptr ==
&EG(uninitialized_zval_ptr)) {
            ……
        }
        // foreach一个object
        else if (Z_TYPE_PP(array_ptr_ptr) == IS_OBJECT) {
            ……
        }
        else {
            // 本例会进入该分支
            if (Z_TYPE_PP(array_ptr_ptr) == IS_ARRAY) {
                // 注意此处的SEPARATE_ZVAL_IF_NOT_REF
                // 它会重新复制一个数组出来
                // 真正分离$tmp和$arr,变成了内存中的2个数组
                SEPARATE_ZVAL_IF_NOT_REF(array_ptr_ptr);
                if (opline->extended_value & ZEND_FE_FETCH_BYREF)
{
                    Z_SET_ISREF_PP(array_ptr_ptr);
                }
            }
            array_ptr = *array_ptr_ptr;
            Z_ADDREF_P(array_ptr);
        }
    } else {
        ……
    }

    // 重置数组内部指针
    ……
}

从代码中可以看出,真正执行变量分离并不是在赋值语句执行的时候,而是推迟到了使用变量的时候,这也是Copy
On Write机制在PHP中的实现。
FE_RESET之后,内存的变化如下:
奥门新浦京官方网站 11

上图解释了为何foreach并不会对原来的$arr产生影响。至于ref_count以及is_ref的变化情况,感兴趣的同学可以详细阅读ZEND_FE_RESET_SPEC_CV_HANDLER和ZEND_SWITCH_FREE_SPEC_VAR_HANDLER的具体实现(均位于php-src/zend/zend_vm_execute.h中),本文不做详细剖析:)

发表评论

电子邮件地址不会被公开。 必填项已用*标注