实际此过程可以认为是3.3.3.4的一部分,这个阶段就是函数调用结束,返回调用处的过程,这个过程中有三个关键工作:拷贝返回值、执行器切回调用位置、释放清理局部变量。
上面例子此过程opcode为ZEND_RETURN,对应的handler为ZEND_RETURN_SPEC_CV_HANDLER:
static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_RETURN_SPEC_CV_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
USE_OPLINE
zval *retval_ptr;
zend_free_op free_op1;
//获取返回值
retval_ptr = _get_zval_ptr_cv_undef(execute_data, opline->op1.var);
if (IS_CV == IS_CV && UNEXPECTED(Z_TYPE_INFO_P(retval_ptr) == IS_UNDEF)) {
//返回值未定义,返回NULL
retval_ptr = GET_OP1_UNDEF_CV(retval_ptr, BP_VAR_R);
if (EX(return_value)) {
ZVAL_NULL(EX(return_value));
}
} else if(!EX(return_value)){
//无返回值
...
}else{ //返回值正常
...
ZVAL_DEREF(retval_ptr); //如果retval_ptr是引用则将找到其具体引用的zval
ZVAL_COPY(EX(return_value), retval_ptr); //将返回值复制给调用方接收值:EX(return_value)
...
}
ZEND_VM_TAIL_CALL(zend_leave_helper_SPEC(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU));
}
继续看下zend_leave_helper_SPEC,执行器切换、局部变量清理就是在这个函数中完成的。
static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL zend_leave_helper_SPEC(ZEND_OPCODE_HANDLER_ARGS)
{
zend_execute_data *old_execute_data;
uint32_t call_info = EX_CALL_INFO();
if (EXPECTED(ZEND_CALL_KIND_EX(call_info) == ZEND_CALL_NESTED_FUNCTION)) {
//普通的函数调用将走到这个分支
i_free_compiled_variables(execute_data);
...
}
//include、eval及整个脚本的结束(main函数)走到下面
//...
//将执行器切回调用的位置
EG(current_execute_data) = EX(prev_execute_data);
}
//zend_execute.c
//清理局部变量的过程
static zend_always_inline void i_free_compiled_variables(zend_execute_data *execute_data)
{
zval *cv = EX_VAR_NUM(0);
zval *end = cv + EX(func)->op_array.last_var;
while (EXPECTED(cv != end)) {
if (Z_REFCOUNTED_P(cv)) {
if (!Z_DELREF_P(cv)) { //引用计数减一后为0
zend_refcounted *r = Z_COUNTED_P(cv);
ZVAL_NULL(cv);
zval_dtor_func_for_ptr(r); //释放变量值
} else {
GC_ZVAL_CHECK_POSSIBLE_ROOT(cv); //引用计数减一后>0,启动垃圾检查机制,清理循环引用导致无法回收的垃圾
}
}
cv++;
}
}
除了函数调用完成时有return操作,其它还有两种情况也会有此过程:
所以实际上面说的这两种情况属于一类,它们并不是局部变量的清理,而是 全局变量的清理 ,另外局部变量的清理也并非只有return一个时机,另外还有一个更重要的时机就是变量分离时,这种情况我们在《PHP语法实现》一节再具体说明。