附录2:defer推迟函数调用语法的实现

使用过Go语言的应该都知道defer这个语法,它用来推迟一个函数的执行,在函数执行返回前首先检查当前函数内是否有推迟执行的函数,如果有则执行,然后再返回。defer是一个非常有用的语法,这个功能可以很方便的在函数结束前执行一些清理工作,比如关闭打开的文件、关闭连接、释放资源、解锁等等。这样延迟一个函数有以下两个好处:

  • (1) 靠近使用位置,避免漏掉清理工作,同时比放在函数结尾要清晰
  • (2) 如果有多处返回的地方可以避免代码重复,比如函数中有很多处return

在一个函数中可以使用多个defer,其执行顺序与栈类似:后进先出,先定义的defer后执行。另外,在返回之后定义的defer将不会被执行,只有返回前定义的才会执行,通过exit退出程序的情况也不会执行任何defer。

在PHP中并没有实现类似的语法,本节我们将尝试在PHP中实现类似Go语言中defer的功能。此功能的实现需要对PHP的语法解析、抽象语法树/opcode的编译、opcode指令的执行等环节进行改造,涉及的地方比较多,但是改动点比较简单,可以很好的帮助大家完整的理解PHP编译、执行两个核心阶段的实现。总体实现思路:

  • (1)语法解析: defer本质上还是函数调用,只是将调用时机移到了函数的最后,所以编译时可以复用调用函数的规则,但是需要与普通的调用区分开,所以我们新增一个AST节点类型,其子节点为为正常函数调用编译的AST,语法我们定义为:defer function_name();
  • (2)opcode编译: 编译opcode时也复用调用函数的编译逻辑,不同的地方在于把defer放在最后编译,另外需要在编译return前新增一条opcode,用于执行return前跳转到defer开始的位置,在defer的最后也需要新增一条opcode,用于执行完defer后跳回return的位置;
  • (3)执行阶段: 执行时如果发现是return前新增的opcode则跳转到defer开始的位置,同时把return的位置记录下来,执行完defer后再跳回return。

编译后的opcode指令如下图所示:

附录2:defer推迟函数调用语法的实现 - 图1

接下来我们详细介绍下各个环节的改动,一步步实现defer功能。

(1)语法解析

想让PHP支持defer function_name()的语法首先需要修改的是词法解析规则,将”defer”关键词解析为token:T_DEFER,这样词法扫描器在匹配token时遇到”defer”将告诉语法解析器这是一个T_DEFER。这一步改动比较简单,PHP的词法解析规则定义在zend_language_scanner.l中,加入以下代码即可:

  1. <ST_IN_SCRIPTING>"defer" {
  2. RETURN_TOKEN(T_DEFER);
  3. }

完成词法解析规则的修改后接着需要定义语法解析规则,这是非常关键的一步,语法解析器会根据配置的语法规则将PHP代码解析为抽象语法树(AST)。普通函数调用会被解析为ZEND_AST_CALL类型的AST节点,我们新增一种节点类型:ZEND_AST_DEFER_CALL,抽象语法树的节点类型为enum,定义在zend_ast.h中,同时此节点只需要一个子节点,这个子节点用于保存ZEND_AST_CALL节点,因此zend_ast.h的修改如下:

  1. enum _zend_ast_kind {
  2. ...
  3. /* 1 child node */
  4. ...
  5. ZEND_AST_DEFER_CALL
  6. ....
  7. }

定义完AST节点后就可以在配置语法解析规则了,把defer语法解析为ZEND_AST_DEFER_CALL节点,我们把这条语法规则定义在”statement:”节点下,if、echo、for等语法都定义在此节点下,语法解析规则文件为zend_language_parser.y:

  1. statement:
  2. '{' inner_statement_list '}' { $$ = $2; }
  3. ...
  4. | T_DEFER function_call ';' { $$ = zend_ast_create(ZEND_AST_DEFER_CALL, $2); }
  5. ;

修改完这两个文件后需要分别调用re2c、yacc生成对应的C文件,具体的生成命令可以在Makefile.frag中看到:

  1. $ re2c --no-generation-date --case-inverted -cbdFt Zend/zend_language_scanner_defs.h -oZend/zend_language_scanner.c Zend/zend_language_scanner.l
  2. $ yacc -p zend -v -d Zend/zend_language_parser.y -oZend/zend_language_parser.c

执行完以后将在Zend目录下重新生成zend_language_scanner.c、zend_language_parser.c两个文件。到这一步已经完成生成抽象语法树的工作了,重新编译PHP后已经能够解析defer语法了,将会生成以下节点:

附录2:defer推迟函数调用语法的实现 - 图2

(2)编译ZEND_AST_DEFER_CALL

生成抽象语法树后接下来就是编译生成opcodes的操作,即从AST->Opcodes。编译ZEND_AST_DEFER_CALL节点时不能立即进行编译,需要等到当前脚本或函数全部编译完以后再进行编译,所以在编译过程需要把ZEND_AST_DEFER_CALL节点先缓存下来,参考循环结构编译时生成的zend_brk_cont_element的存储位置,我们也把ZEND_AST_DEFER_CALL节点保存在zend_op_array中,通过数组进行存储,将ZEND_AST_DEFER_CALL节点依次存入该数组,zend_op_array中加入以下几个成员:

  • last_defer: 整形,记录当前编译的defer数
  • defer_start_op: 整形,用于记录defer编译生成opcode指令的起始位置
  • defer_call_array: 保存ZEND_AST_DEFER_CALL节点的数组,用于保存ast节点的地址
  1. struct _zend_op_array {
  2. ...
  3. int last_defer;
  4. uint32_t defer_start_op;
  5. zend_ast **defer_call_array;
  6. }

修改完数据结构后接着对应修改zend_op_array初始化的过程:

  1. //zend_opcode.c
  2. void init_op_array(zend_op_array *op_array, zend_uchar type, int initial_ops_size)
  3. {
  4. ...
  5. op_array->last_defer = 0;
  6. op_array->defer_start_op = 0;
  7. op_array->defer_call_array = NULL;
  8. ...
  9. }

完成依赖的这些数据结构的改造后接下来开始编写具体的编译逻辑,也就是编译ZEND_AST_DEFER_CALL的处理。抽象语法树的编译入口函数为zend_compile_top_stmt(),然后根据不同节点的类型进行相应的编译,我们在zend_compile_stmt()函数中对ZEND_AST_DEFER_CALL节点进行编译:

  1. void zend_compile_stmt(zend_ast *ast)
  2. {
  3. ...
  4. switch (ast->kind) {
  5. ...
  6. case ZEND_AST_DEFER_CALL:
  7. zend_compile_defer_call(ast);
  8. break
  9. ...
  10. }
  11. }

编译过程只是将ZEND_AST_DEFER_CALL的子节点(即:ZEND_AST_CALL)保存到zend_op_array->defer_call_array数组中,注意这里defer_call_array数组还没有分配内存,参考循环结构的实现,这里我们定义了一个函数用于数组的分配:

  1. //zend_compile.c
  2. void zend_compile_defer_call(zend_ast *ast)
  3. {
  4. if(!ast){
  5. return;
  6. }
  7. zend_ast **call_ast = NULL;
  8. //将普通函数调用的ast节点保存到defer_call_array数组中
  9. call_ast = get_next_defer_call(CG(active_op_array));
  10. *call_ast = ast->child[0];
  11. }
  12. //zend_opcode.c
  13. zend_ast **get_next_defer_call(zend_op_array *op_array)
  14. {
  15. op_array->last_defer++;
  16. op_array->defer_call_array = erealloc(op_array->defer_call_array, sizeof(zend_ast*)*op_array->last_defer);
  17. return &op_array->defer_call_array[op_array->last_defer-1];
  18. }

既然分配了defer_call_array数组的内存就需要在zend_op_array销毁时释放:

  1. //zend_opcode.c
  2. ZEND_API void destroy_op_array(zend_op_array *op_array)
  3. {
  4. ...
  5. if (op_array->defer_call_array) {
  6. efree(op_array->defer_call_array);
  7. }
  8. ...
  9. }

编译完整个脚本或函数后,最后还会编译一条ZEND_RETURN,也就是返回指令,相当于ret指令,注意:这条opcode并不是我们在脚本中定义的return语句的,而是PHP内核为我们加的一条指令,这就是为什么有些函数我们没有写return也能返回的原因,任何函数或脚本都会生成这样一条指令。我们缓存在zend_op_array->defer_call_array数组中defer就是要在这时进行编译,也就是把defer的指令编译在最后。内核最后编译返回的这条指令由zend_emit_final_return()方法完成,我们把defer的编译放在此方法的末尾:

  1. //zend_compile.c
  2. void zend_emit_final_return(zval *zv)
  3. {
  4. ...
  5. ret = zend_emit_op(NULL, returns_reference ? ZEND_RETURN_BY_REF : ZEND_RETURN, &zn, NULL);
  6. ret->extended_value = -1;
  7. //编译推迟执行的函数调用
  8. zend_emit_defer_call();
  9. }

前面已经说过,defer本质上就是函数调用,所以编译的过程直接复用普通函数调用的即可。另外,在编译时把起始位置记录到zend_op_array->defer_start_op中,因为在执行return前需要知道跳转到什么位置,这个值就是在那时使用的,具体的用法稍后再作说明。编译时按照倒序的顺序进行编译:

  1. //zend_compile.c
  2. void zend_emit_defer_call()
  3. {
  4. if (!CG(active_op_array)->defer_call_array) {
  5. return;
  6. }
  7. zend_ast *call_ast;
  8. zend_op *nop;
  9. znode result;
  10. uint32_t opnum = get_next_op_number(CG(active_op_array));
  11. int defer_num = CG(active_op_array)->last_defer;
  12. //记录推迟的函数调用指令开始位置
  13. CG(active_op_array)->defer_start_op = opnum;
  14. while(--defer_num >= 0){
  15. call_ast = CG(active_op_array)->defer_call_array[defer_num];
  16. if (call_ast == NULL) {
  17. continue;
  18. }
  19. nop = zend_emit_op(NULL, ZEND_NOP, NULL, NULL);
  20. nop->op1.var = -2;
  21. //编译函数调用
  22. zend_compile_call(&result, call_ast, BP_VAR_R);
  23. }
  24. //compile ZEND_DEFER_CALL_END
  25. zend_emit_op(NULL, ZEND_DEFER_CALL_END, NULL, NULL);
  26. }

编译完推迟的函数调用之后,编译一条ZEND_DEFER_CALL_END指令,该指令用于执行完推迟的函数后跳回return的位置进行返回,opcode定义在zend_vm_opcodes.h中:

  1. //zend_vm_opcodes.h
  2. #define ZEND_DEFER_CALL_END 174

还有一个地方你可能已经注意到,在逐个编译defer的函数调用前都生成了一条ZEND_NOP的指令,这个的目的是什么呢?开始的时候已经介绍过defer语法的特点,函数中定义的defer并不是全部执行,在return之后定义的defer是不会执行的,比如:

  1. func main(){
  2. defer fmt.Println("A")
  3. if 1 == 1{
  4. return
  5. }
  6. defer fmt.Println("B")
  7. }

这种情况下第2个defer就不会生效,因此在return前跳转的位置就不一定是zend_op_array->defer_start_op,有可能会跳过几个函数的调用,所以这里我们通过ZEND_NOP这条空指令对多个defer call进行隔离,同时为避免与其它ZEND_NOP指令混淆,增加一个判断条件:op1.var=-2。这样在return前跳转时就根据此前定义的defer数跳过部分函数的调用,如下图所示。

附录2:defer推迟函数调用语法的实现 - 图3

到这一步我们已经完成defer函数调用的编译,此时重新编译PHP后可以看到通过defer推迟的函数调用已经被编译在最后了,只不过这个时候它们不能被执行。

(3)编译return

编译return时需要插入一条指令用于跳转到推迟执行的函数调用指令处,因此这里需要再定义一条opcode:ZEND_DEFER_CALL,在编译过程中defer call还未编译,因此此时还无法知道具体的跳转值。

  1. //zend_vm_opcodes.h
  2. #define ZEND_DEFER_CALL 173
  3. #define ZEND_DEFER_CALL_END 174

PHP脚本中声明的return语句由zend_compile_return()方法完成编译,在编译生成ZEND_DEFER_CALL指令时还需要将当前已定义的defer数(即在return前声明的defer)记录下来,用于计算具体的跳转值。

  1. void zend_compile_return(zend_ast *ast)
  2. {
  3. ...
  4. //在return前编译ZEND_DEFER_CALL:用于在执行retur前跳转到defer call
  5. if (CG(active_op_array)->defer_call_array) {
  6. defer_zn.op_type = IS_UNUSED;
  7. defer_zn.u.op.num = CG(active_op_array)->last_defer;
  8. zend_emit_op(NULL, ZEND_DEFER_CALL, NULL, &defer_zn);
  9. }
  10. //编译正常返回的指令
  11. opline = zend_emit_op(NULL, by_ref ? ZEND_RETURN_BY_REF : ZEND_RETURN,
  12. &expr_node, NULL);
  13. ...
  14. }

除了这种return外还有一种我们上面已经提过的return,即PHP内核编译的return指令,当PHP脚本中没有声明return语句时将执行内核添加的那条指令,因此也需要在zend_emit_final_return()加上上面的逻辑。

  1. void zend_emit_final_return(zval *zv)
  2. {
  3. ...
  4. //在return前编译ZEND_DEFER_CALL:用于在执行retur前跳转到defer call
  5. if (CG(active_op_array)->defer_call_array) {
  6. //当前return之前定义的defer数
  7. defer_zn.op_type = IS_UNUSED;
  8. defer_zn.u.op.num = CG(active_op_array)->last_defer;
  9. zend_emit_op(NULL, ZEND_DEFER_CALL, NULL, &defer_zn);
  10. }
  11. //编译返回指令
  12. ret = zend_emit_op(NULL, returns_reference ? ZEND_RETURN_BY_REF : ZEND_RETURN, &zn, NULL);
  13. ret->extended_value = -1;
  14. //编译推迟执行的函数调用
  15. zend_emit_defer_call();
  16. }

(4)计算ZEND_DEFER_CALL指令的跳转位置

前面我们已经完成了推迟调用函数以及return编译过程的改造,在编译完成后ZEND_DEFER_CALL指令已经能够知道具体的跳转位置了,因为推迟调用的函数已经编译完成了,所以下一步就是为全部的ZEND_DEFER_CALL指令计算跳转值。前面曾介绍过,在编译完成有一个pass_two()的环节,我们就在这里完成具体跳转位置的计算,并把跳转位置保存到ZEND_DEFER_CALL指令的操作数中,在执行阶段直接跳转到对应位置。

  1. ZEND_API int pass_two(zend_op_array *op_array)
  2. {
  3. zend_op *opline, *end;
  4. ...
  5. //遍历opcode
  6. opline = op_array->opcodes;
  7. end = opline + op_array->last;
  8. while (opline < end) {
  9. switch (opline->opcode) {
  10. ...
  11. case ZEND_DEFER_CALL: //设置jmp
  12. {
  13. uint32_t defer_start = op_array->defer_start_op;
  14. //skip_defer为当前return之后声明的defer数,也就是不需要执行的defer
  15. uint32_t skip_defer = op_array->last_defer - opline->op2.num;
  16. //defer_opline为推迟的函数调用起始位置
  17. zend_op *defer_opline = op_array->opcodes + defer_start;
  18. uint32_t n = 0;
  19. while(n <= skip_defer){
  20. if (defer_opline->opcode == ZEND_NOP && defer_opline->op1.var == -2) {
  21. n++;
  22. }
  23. defer_opline++;
  24. defer_start++;
  25. }
  26. //defer_start为opcode在op_array->opcodes数组中的位置
  27. opline->op1.opline_num = defer_start;
  28. //将跳转位置保存到操作数op1中
  29. ZEND_PASS_TWO_UPDATE_JMP_TARGET(op_array, opline, opline->op1);
  30. }
  31. break;
  32. }
  33. ...
  34. }
  35. ...
  36. }

这里我们并没有直接编译为ZEND_JMP跳转指令,虽然ZEND_JMP可以跳转到后面的指令位置,但是最后的那条跳回return位置的指令(即:ZEND_DEFER_CALL_END)由于可能存在多个return的原因无法在编译期间确定具体的跳转值,只能在运行期间执行ZEND_DEFER_CALL时才能确定,所以需要在ZEND_DEFER_CALL指令的handler中将return的位置记录下来,执行ZEND_DEFER_CALL_END时根据这个值跳回。

(5)定义ZEND_DEFER_CALL、ZEND_DEFER_CALL_END指令的handler

ZEND_DEFER_CALL指令执行时需要将return的位置保存下来,我们把这个值保存到zend_execute_data结构中:

  1. //zend_compile.h
  2. struct _zend_execute_data {
  3. ...
  4. const zend_op *return_opline;
  5. ...
  6. }

opcode的handler定义在zend_vm_def.h文件中,定义完成后需要执行php zend_vm_gen.php脚本生成具体的handler函数。

  1. ZEND_VM_HANDLER(173, ZEND_DEFER_CALL, ANY, ANY)
  2. {
  3. USE_OPLINE
  4. //1) 将return指令的位置保存到EX(return_opline)
  5. EX(return_opline) = opline + 1;
  6. //2) 跳转
  7. ZEND_VM_SET_OPCODE(OP_JMP_ADDR(opline, opline->op1));
  8. ZEND_VM_CONTINUE();
  9. }
  10. ZEND_VM_HANDLER(174, ZEND_DEFER_CALL_END, ANY, ANY)
  11. {
  12. USE_OPLINE
  13. ZEND_VM_SET_OPCODE(EX(return_opline));
  14. ZEND_VM_CONTINUE();
  15. }

到目前为止我们已经完成了全部的修改,重新编译PHP后就可以使用defer语法了:

  1. function shutdown($a){
  2. echo $a."\n";
  3. }
  4. function test(){
  5. $a = 1234;
  6. defer shutdown($a);
  7. $a = 8888;
  8. if(1){
  9. return "mid end\n";
  10. }
  11. defer shutdown("9999");
  12. return "last end\n";
  13. }
  14. echo test();

执行后将显示:

  1. 8888
  2. mid end

这里我们只实现了普通函数调用的方式,关于成员方法、静态方法、匿名函数等调用方式并未实现,留给有兴趣的读者自己去实现。

完整代码:https://github.com/pangudashu/php-7.0.12