跟try有关的外部通用指令有四条,分别是
{"try", 2, -1, 0, 7521}, //特定指令id为-1,指令转换操作的地址偏移为7521
{"try_end", 1, 461, 1, -1}, //对应特定指令id为461
{"try_case", 1, 459, 1, -1}, //对应特定指令id为459
{"try_case_end", 1, 460, 1, -1}, //对应特定指令id为460
我们发现try指令对应的特定指令为-1,也就是说try指令不是直接转换为特殊指令的,而是经由加载器进行一系列操作 ,操作的地址偏移为7521。其对应的操作如下:
TOP_rename, 62 //TOP_rename 的实现在beam_load.c中:
case TOP_rename:
instr->op = op = *pc++; //op 赋值为62
instr->arity = gen_opc[op].arity;//gen_opc[62] 为 {"catch", 2, 36, 1, -1},
return TE_OK;
也就是说,try外部通用指令会被转换为catch指令。
跟catch有关的外部通用指令有两条,分别是:
{"catch", 2, 36, 1, -1}, //对应的特定指令id为36
{"catch_end", 1, 37, 1, -1}, //对应的特定指令id为37
catch特定指令的源码实现如下:
#define yb(N) (*ADD_BYTE_OFFSET(E, N))// E 为 c_p->stop 即进程堆栈空间的结束位置
#define ADD_BYTE_OFFSET(ptr, offset) ((Eterm *) (((unsigned char *)ptr) + (offset)))
OpCase(catch_yf)://有删减
{
BeamInstr next_pf = BeamCodeAddr(I[3]); //获取下一条指令的地址
c_p->catches++;//cp是当前进程的结构,包含当前进程的各种信息(见 erl_process.h process定义)
yb(I[1]) = I[2];//I[1] 和 I[2] 参见下文的final_touch函数执行完毕后的 codev[ci+1],codev[ci+2]
GotoPF(next_pf);//执行下一条指令
}
catch_end特定指令的源码实现如下:
OpCase(catch_end_y)://有删减
{
BeamInstr next_pf = BeamCodeAddr(I[1]); //获取下一条指令的地址
c_p->catches--; //cp对catch的记录减一
make_blank(yb(BeamExtraData(I[0])));;//记录当前 catch_end地址的配对的catch指令的记录置空
if (is_non_value(r(0))) {//如果上条指令执行的结果,不是正常的返回值(也就是抛错了)
if (x(1) == am_throw) {
r(0) = x(2);//如果是 throw 的抛错,则返回值为 throw(Info) 中的 Info
} else {
if (x(1) == am_error) {//如果是 error 的抛错
x(2) = add_stacktrace(c_p, x(2), x(3)); //记录错误到cp的跟踪堆栈中
}
//此处省略了剩余空间判断,以及空间不足时进行垃圾回收的逻辑
//生成 {'EXIT',当前进程跟踪堆栈信息},作为返回值
r(0) = TUPLE2(HTOP, am_EXIT, x(2));
}
}
GotoPF(next_pf);//执行下一条指令
}
try_end特定指令的源码实现如下:
OpCase(try_end_y)://有删减
{
BeamInstr next_pf = BeamCodeAddr(I[1]);//获取下一条指令的地址
c_p->catches--;//进程结构里的catch记录减一
make_blank(yb(BeamExtraData(I[0])));//记录当前 catch_end地址的配对的catch指令的记录置空
GotoPF(next_pf);//执行下一条指令
}
try_case特定指令的源码实现如下:
//try catch语句
try Exprs of
Pattern1 [when GuardSeq1] ->
Body1;
...;
PatternN [when GuardSeqN] ->
BodyN
catch
Class1:ExceptionPattern1[:Stacktrace] [when ExceptionGuardSeq1] ->
ExceptionBody1;
...;
ClassN:ExceptionPatternN[:Stacktrace] [when ExceptionGuardSeqN] ->
ExceptionBodyN
after
AfterBody
end
OpCase(try_case_y)://有删减
{
BeamInstr next_pf = BeamCodeAddr(I[1]);//获取下一条指令的地址
c_p->catches--;//进程结构里的catch记录减一
make_blank(yb(BeamExtraData(I[0])));;//记录当前 catch_end地址的配对的catch指令的记录置空
//x(1)的值为抛错类型: error throw exit,即上述结构中的 ClassN
//x(2)的值为抛错内容,如 throw(Info) 中的Info,即上述结构中的 Exception
//x(3)的值为构建Stacktrace的编码信息,Stacktrace = build_stacktrace(c_p, x(3))
r(0) = x(1);//r(0)为x(0),此三条命令调整返回值顺序
x(1) = x(2);
x(2) = x(3);
GotoPF(next_pf);//执行下一条指令
}
try_case_end特定指令的源码实现如下:
OpCase(try_case_end_s): //当 try catch 语句的 Pattern 全都匹配不上时,执行的操作
{
Eterm targ1;
GetSource(I[1], targ1); //获取x(I[1])或y(I[1])中的数据赋值给targ1
{
c_p->fvalue = targ1;//cp进程结构 记录错误信息
c_p->freason = EXC_TRY_CLAUSE;//cp进程结构 记录错误原因
goto find_func_info;//执行 find_func_info 指令
}
}
加载器在加载到catch指令时,会做一些额外的处理:
//code为虚拟机执行的指令数组,可以理解为代码区
case op_catch_yf:
//此时codev内容为:
//codev[ci-3]:catch_yf指令或catch_yf指令地址
//codev[ci-2]:catch配对的结束指令地址存储在y(X),中的X
//codev[ci-1]:catch配对的结束指令的label值,即存储在y(X)中的数据
code[ci-3] = stp->catches; // stp->catches记录了上一条catch指令在code中的偏移
stp->catches = ci-3; // 将当前catch在code中的偏移记录到stp->catches
//此时codev内容为:
//codev[ci-3]:上一条catch指令在code中的偏移
//codev[ci-2],codev[ci-1]:同上
break;
之后加载器在执行freeze_code函数的逻辑时,会调整 codev[ci-1] 的值为 catch结束指令和catch开始指令的地址差值
最后加载器在加载完毕时调用 erts_finish_loading 函数 → 调用 final_touch 函数,其中有逻辑:
index = stp->catches;//获取最后一条catch指令,在codev中的偏移
catches = BEAM_CATCHES_NIL;
while (index != 0) {
//此时codev[index]:上一条catch指令在code中的偏移
BeamInstr next = codev[index];
codev[index] = ((BeamInstr)(op_catch_yf));// 向 code[偏移] 写入catch指令
BeamInstr* abs_addr;
//此时codev内容为:
//codev[index]:((BeamInstr)(op_catch_yf))
//codev[index+1]:catch配对的结束指令地址存储在y(X),中的X
//codev[index+2]:catch结束指令和catch开始指令的地址差值
abs_addr = (BeamInstr *)codev + index + codev[index+2]; //得到catch结束指令地址
catches = beam_catches_cons(abs_addr, catches);
//beam_catches_cons 将 abs_addr和catches 记录在一个全局数组中,并返回数组序号
codev[index+2] = make_catch(catches);//将catches左移六位后,低6位赋值为011011,即标记当前Eterm为catch类型
//此时codev[ci+2]:标记为catch的Eterm, 其值右移6位后,得到值是一个索引,根据该索引可查询到code[ci]对应的catch结束语句所在的位置
index = next;
}
inst_p->catches = catches;
当程序抛错时,查询catch结束位置的核心逻辑如下:
next_catch(Process* c_p, Eterm *reg) {
int active_catches = c_p->catches > 0; //判断当前进程记录的catch数是否大于0
Eterm *ptr, *prev = NULL;
ptr = c_p->stop;//注: y[0] 即是 c_p->stop[0]
while (ptr < c_p->hend) {//注:进程初始化时, hend = heap+sz, stop = hend -1, htop = heap
if (is_catch(*ptr)) {//遍历栈区,获取第一个类型位catch的Eterm
if (active_catches) beam_catches_car(catch_val((*ptr)));//根据该Eterm,查询abs_addr(见上文)
ptr++;
}
else ptr++;
}
return NULL;