Skip to content

Commit e09183e

Browse files
committed
update
1 parent 8552cec commit e09183e

File tree

2 files changed

+147
-0
lines changed

2 files changed

+147
-0
lines changed

img/defer_call.png

25.6 KB
Loading

try/defer.md

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,4 +54,151 @@ $ yacc -p ini_ -v -d Zend/zend_language_parser.y -oZend/zend_language_parser.c
5454

5555
![](../img/defer_ast.png)
5656

57+
__(2)编译ZEND_AST_DEFER_CALL__
58+
59+
生成抽象语法树后接下来就是编译生成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中加入以下几个成员:
60+
61+
* __last_defer:__ 整形,记录当前编译的defer数
62+
* __defer_start_op:__ 整形,用于记录defer编译生成opcode指令的起始位置
63+
* __defer_call_array:__ 保存ZEND_AST_DEFER_CALL节点的数组,用于保存ast节点的地址
64+
65+
```c
66+
struct _zend_op_array {
67+
...
68+
int last_defer;
69+
uint32_t defer_start_op;
70+
zend_ast **defer_call_array;
71+
}
72+
```
73+
修改完数据结构后接着对应修改zend_op_array初始化的过程:
74+
```c
75+
//zend_opcode.c
76+
void init_op_array(zend_op_array *op_array, zend_uchar type, int initial_ops_size)
77+
{
78+
...
79+
op_array->last_defer = 0;
80+
op_array->defer_start_op = 0;
81+
op_array->defer_call_array = NULL;
82+
...
83+
}
84+
```
85+
完成依赖的这些数据结构的改造后接下来开始编写具体的编译逻辑,也就是编译ZEND_AST_DEFER_CALL的处理。抽象语法树的编译入口函数为zend_compile_top_stmt(),然后根据不同节点的类型进行相应的编译,我们在zend_compile_stmt()函数中对ZEND_AST_DEFER_CALL节点进行编译:
86+
```c
87+
void zend_compile_stmt(zend_ast *ast)
88+
{
89+
...
90+
switch (ast->kind) {
91+
...
92+
case ZEND_AST_DEFER_CALL:
93+
zend_compile_defer_call(ast);
94+
break
95+
...
96+
}
97+
}
98+
```
99+
编译过程只是将ZEND_AST_DEFER_CALL的子节点(即:ZEND_AST_CALL)保存到zend_op_array->defer_call_array数组中,注意这里defer_call_array数组还没有分配内存,参考循环结构的实现,这里我们定义了一个函数用于数组的分配:
100+
```c
101+
//zend_compile.c
102+
void zend_compile_defer_call(zend_ast *ast)
103+
{
104+
if(!ast){
105+
return;
106+
}
107+
108+
zend_ast **call_ast = NULL;
109+
//将普通函数调用的ast节点保存到defer_call_array数组中
110+
call_ast = get_next_defer_call(CG(active_op_array));
111+
*call_ast = ast->child[0];
112+
}
113+
114+
//zend_opcode.c
115+
zend_ast **get_next_defer_call(zend_op_array *op_array)
116+
{
117+
op_array->last_defer++;
118+
op_array->defer_call_array = erealloc(op_array->defer_call_array, sizeof(zend_ast*)*op_array->last_defer);
119+
return &op_array->defer_call_array[op_array->last_defer-1];
120+
}
121+
```
122+
既然分配了defer_call_array数组的内存就需要在zend_op_array销毁时释放:
123+
```c
124+
//zend_opcode.c
125+
ZEND_API void destroy_op_array(zend_op_array *op_array)
126+
{
127+
...
128+
if (op_array->defer_call_array) {
129+
efree(op_array->defer_call_array);
130+
}
131+
...
132+
}
133+
```
134+
编译完整个脚本或函数后,最后还会编译一条ZEND_RETURN,也就是返回指令,相当于ret指令,注意:这条opcode并不是我们在脚本中定义的return语句的,而是PHP内核为我们加的一条指令,这就是为什么有些函数我们没有写return也能返回的原因,任何函数或脚本都会生成这样一条指令。我们缓存在zend_op_array->defer_call_array数组中defer就是要在这时进行编译,也就是把defer的指令编译在最后。内核最后编译返回的这条指令由zend_emit_final_return()方法完成,我们把defer的编译放在此方法的末尾:
135+
```c
136+
//zend_compile.c
137+
void zend_emit_final_return(zval *zv)
138+
{
139+
...
140+
ret = zend_emit_op(NULL, returns_reference ? ZEND_RETURN_BY_REF : ZEND_RETURN, &zn, NULL);
141+
ret->extended_value = -1;
142+
143+
//编译defer call
144+
zend_emit_defer_call();
145+
}
146+
```
147+
前面已经说过,defer本质上就是函数调用,所以编译的过程直接复用普通函数调用的即可。另外,在编译时把起始位置记录到zend_op_array->defer_start_op中,因为在执行return前需要知道跳转到什么位置,这个值就是在那时使用的,具体的用法稍后再作说明。编译时按照倒序的顺序进行编译:
148+
```c
149+
//zend_compile.c
150+
void zend_emit_defer_call()
151+
{
152+
if (!CG(active_op_array)->defer_call_array) {
153+
return;
154+
}
155+
156+
zend_ast *call_ast;
157+
zend_op *nop;
158+
znode result;
159+
uint32_t opnum = get_next_op_number(CG(active_op_array));
160+
int defer_num = CG(active_op_array)->last_defer;
161+
162+
//记录推迟的函数调用指令开始位置
163+
CG(active_op_array)->defer_start_op = opnum;
164+
165+
while(--defer_num >= 0){
166+
call_ast = CG(active_op_array)->defer_call_array[defer_num];
167+
if (call_ast == NULL) {
168+
continue;
169+
}
170+
nop = zend_emit_op(NULL, ZEND_NOP, NULL, NULL);
171+
nop->op1.var = -2;
172+
//编译函数调用
173+
zend_compile_call(&result, call_ast, BP_VAR_R);
174+
}
175+
//compile ZEND_DEFER_CALL
176+
zend_emit_op(NULL, ZEND_DEFER_CALL_END, NULL, NULL);
177+
}
178+
```
179+
编译完推迟的函数调用之后,编译一条ZEND_DEFER_CALL_END指令,该指令用于执行完推迟的函数后跳回return的位置进行返回,opcode定义在zend_vm_opcodes.h中:
180+
```c
181+
//zend_vm_opcodes.h
182+
#define ZEND_DEFER_CALL_END 174
183+
```
184+
还有一个地方你可能已经注意到,在逐个编译defer的函数调用前都生成了一条ZEND_NOP的指令,这个的目的是什么呢?开始的时候已经介绍过defer语法的特点,函数中定义的defer并不是全部执行,在return之后定义的defer是不会执行的,比如:
185+
```go
186+
func main(){
187+
defer fmt.Println("A")
188+
189+
if 1 == 1{
190+
return
191+
}
192+
193+
defer fmt.Println("B")
194+
}
195+
```
196+
这种情况下第2个defer就不会生效,因此在return前跳转的位置就不一定是zend_op_array->defer_start_op,有可能会跳过几个函数的调用,所以这里我们通过ZEND_NOP这条空指令对多个defer call进行隔离,同时为避免与其它ZEND_NOP指令混淆,增加一个判断条件:op1.var=-2。这样在return前跳转时就根据此前定义的defer数跳过部分函数的调用,如下图所示。
197+
198+
![](../img/defer_call.png)
199+
200+
到这一步我们已经完成defer函数调用的编译,此时重新编译PHP后可以看到通过defer推迟的函数调用已经被编译在最后了,只不过这个时候它们不能被执行。
201+
202+
__(3)编译return__
203+
57204

0 commit comments

Comments
 (0)