异常/错误处理
异常指的是在程序运行过程中发生的异常事件,通常由硬件问题或者程序设计问题引起,需要由程序捕获或者处理。在 PHP 7 之前,处理致命错误几乎是不可能的,致命错误不会调用由 set_error_handler() 设置的处理方式,只是停止脚本的执行。PHP 7 对此进行了改进,一些错误也会抛出异常,不捕获仍然报 fatal 错误。PHP 7 对异常或者错误的处理与其他语言类似,使用 try/catch 代码结构,在 catch 中捕获并处理。以下面的 PHP 代码为例,介绍异常机制的底层实现:
<?php
//$a = 1;
try{
//$a->test();
try_statement1
} catch(Exception $e) {
catch_statement2 //echo "exception";
} catch(Throwable $e) {
catch_statement3 // echo "throwable";
} catch(Error $e) {
catch_statement4 // echo "error";
} finally {
finally_statement // echo "finally";
}
如以上代码所示,try 结构由普通表达 statement 构成,catch 和 finally 也有表达式列表。try、catch、finally 关键字会生成 T_TRY、T_CATCH 和 T_FINALLY 的 Token。
为了便于理解,这里用 statement 代替注释中的语句,实际的代码以注释的为准。 |
try、catch、finally 的语法规则如下:
T_TRY '{' inner_statement_list '}' catch_list finally_statement
{ $$ = zend_ast_create(ZEND_AST_TRY, $3, $5, $6); }
catch_list:
/* empty */
{ $$ = zend_ast_create_list(0, ZEND_AST_CATCH_LIST); }
catch_list T_CATCH '(' catch_name_list T_VARIABLE ')' '{' inner_statement_
list '}'
{ $$ = zend_AST_list_add($1, zend_ast_create(ZEND_AST_CATCH, $4, $5, $8)); }
finally_statement:
/* empty */ { $$ = NULL; }
| T_FINALLY '{' inner_statement_list '}' { $$ = $3; }
从规则定义可以看出,try 关键字会创建以 ZEND_AST_TRY 节点为根的 AST,根节点有 3 个孩子节点。catch 关键字会创建一个 kind 为 ZEND_AST_CATCH 的 AST 节点,它有 3 个子节点,用于保存接口名称、变量和 catch_statement 表达式。finally 关键字由于其特殊性,最终都会执行,所以作为普通表达式(statement)挂在 try 的最后一个子节点上。以本节开始的 PHP 代码为例,其最终生成的 AST 如图12-15所示。

可以看出,try、catch、finally 语法生成的 AST 相对复杂,因此编译的过程也相对复杂一些,编译的步骤大致如下。
-
在当前
zend_op_array->try_catch_array
数组中增加一个 zend_try_catch_element 结构的元素,通过数组编译计算当前 try 在 try_catch_array 数组中的位置,实现 try 的嵌套。新增的接口记录的有 try/catch 的 opcode 相关信息,这里还不能确定一些信息,后面编译时更新。zend_try_catch_element 的结构如下:typedef struct _zend_try_catch_element { uint32_t try_op; uint32_t catch_op; uint32_t finally_op; uint32_t finally_end; } zend_try_catch_element;
-
编译 try_statement。如果存在 catch 子节点,会生成一条 ZEND_JMP 的 opcode,用来跳过 catch 节点。前文已多次提到 JMP 指令,想必读者已经对该指令的处理逻辑比较熟悉。
-
如果存在 catch 子节点,遍历编译 catch 的每个子节点。编译 catch 的子节点的步骤大致如下。
-
① 检查其是否为第一个 catch,如果是则将当前的 opcode 位置更新到步骤1)中的
zend_try_catch_element->catch_op
中,建立跳转联系。 -
② 编译 ZEND_CATCH 的 opcode。该 opcode 保存着 exception class 相关信息。从图12-15可以看出,exception class 类可以有多个。ZEND_AST_NAME_LIST 是一个 list 类型的节点,这里会检查当前 exception class 是否是最后一个,如果不是,会再生成一条 ZEND_JMP 的 opcode,用来跳到其他的 exception class 的位置。此时并不能确定下一个 exception class 的 opcode 位置。如下情况就包含两种 exception class:
<?php $a = 1; try{ $a->test(); } catch(Exception | Error $e) { echo "exception or error"; }
-
③ 编译 catch_statement,即 catch 节点中的普通表达式。
-
④ 检查其是否是最后一个 catch 节点,如果不是则编译 ZEND_JMP 的 opcode,用来跳过后面的 catch 节点。
-
-
catch 子树编译完成后,更新步骤2)和步骤3)中需要跳过的 catch 节点的 opcode 数。
-
检查是否存在 finally 子树,如果不存在,编译结束;否则编译 finally 节点。编译生成 ZEND_FAST_CALL 和 ZEND_JMP 的 opcode,然后编译 finally_statement,即 finally 中的普通表达式,最后编译 ZEND_FAST_RET 的 opcode。
try catch finally 语法编译生成 opcodes 的函数是 zend_compile_try,这里仍然只保留关键代码,感兴趣的读者可以自行阅读完整代码。
void zend_compile_try(zend_AST *AST) /* {{{ */
{
/*在zend_op_array里面为try增加一个zend_try_catch_element元素*/
try_catch_offset = zend_add_try_element(get_next_op_number(CG(active_op_
array)));
/*编译try节点*/
zend_compile_stmt(try_AST);
/*编译zend_jmp*/
if (catches->children ! = 0) {
jmp_opnums[0] = zend_emit_jump(0);
}
/*遍历每个catch节点,对catch节点进行编译*/
for (i = 0; i < catches->children; ++i) {
/*为每个exception class编译ZEND_CATCH的opcode,除了最后一个exception class之外,额
外编译一条ZEND_JMP的opcode*/
for (j = 0; j < classes->children; j++) {
/*更新zend_try_catch_element结构中的catch_op*/
opnum_catch = get_next_op_number(CG(active_op_array));
if (i == 0 && j == 0) {
CG(active_op_array)->try_catch_array[try_catch_offset].catch_op = opnum_
catch;
}
/*编译ZEND_CATCH*/
opline = get_next_op(CG(active_op_array));
opline->opcode = ZEND_CATCH;
if (! is_lAST_class) {
/*非最后一个exception class,编译ZEND_JMP*/
jmp_multicatch[j] = zend_emit_jump(0);
opline->extended_value = get_next_op_number(CG(active_op_array));
}
}
/*如果当前catch的节点存在多个exception class更新ZEND_JMP跳的opcode的数*/
for (j = 0; j < classes->children -1; j++) {
zend_update_jump_target_to_next(jmp_multicatch[j]);
}
efree(jmp_multicatch);
/*编译catch中的表达式*/
zend_compile_stmt(stmt_AST);
/*如果不是最后一个catch,编译ZEND_JMP*/
if (! is_lAST_catch) {
jmp_opnums[i + 1] = zend_emit_jump(0);
}
}
/*更新每个catch中ZEND_JMP跳的opcode数*/
for (i = 0; i < catches->children; ++i) {
zend_update_jump_target_to_next(jmp_opnums[i]);
}
if (finally_AST) { //编译finally节点
/*编译ZEND_FAST_CALL*/
opline = zend_emit_op(NULL, ZEND_FAST_CALL, NULL, NULL);
opline->op1.num = try_catch_offset;
/*编译ZEND_JMP*/
zend_emit_op(NULL, ZEND_JMP, NULL, NULL);
/*编译ZEND_FAST_RET*/
opline = zend_emit_op(NULL, ZEND_FAST_RET, NULL, NULL);
opline->op1_type = IS_TMP_VAR;
}
}
虽然异常处理语法编译生成 opcodes 的过程比较复杂,但是执行逻辑比较简单,仍然是顺序执行与跳转逻辑配合执行。先执行 try 中的表达式,如果发生异常,通过 ZEND_JMP 跳转到第一个 catch 生成的 opcode 的位置执行;如果 catch 到异常,执行对应的 catch_statement 表达式,否则跳到下一个 catch 生成的 opcode 的位置继续执行,如此反复。如果存在 finally,在跳出 try 语法之前,会先跳到 finally 生成的 opcode 的位置。执行流程与前文所述语法大同小异,此处不再赘述。