字节码解释执行
字节码的解释执行和AST的解释执行有类似之处,而且更简单,因为树形结构已经展开成顺序了,以栈虚拟机为例,为方便起见,假设所有的指令都在一个指令数组里,每个元素是一个指令对象,有code和arg两个属性,解释器入口:
Object execute(Inst[] inst_list, Object[] func_arg);
由于continue和break已经被jmp指令代替了,这里我们认为execute的执行对应于源语言的一个函数调用,因此返回值就是源语言函数调用的返回,我们可以用null(或一个特殊的对象)来表示异常,func_arg表示对应函数的输入参数
既然是栈虚拟机,就需要一个栈,假设用Stack对象,pop和push两个操作,代码依然用动态类型形式,类型统一为Object:
int idx = 0;
Stack<Object> stk;
for (;;)
{
Inst inst = inst_list[idx]; //如果这里越界,肯定是编译器bug
++ idx;
switch(inst.code)
{
case CODE_LOAD:
{
stk.push(env.get(inst.arg)); //env是运行时环境的抽象
continue;
}
case CODE_STORE:
{
env.set(inst.arg, stk.pop());
continue;
}
...
case CODE_ADD:
{
Object b = stk.pop();
Object a = stk.pop();
stk.push(a.add(b)); //其实这是不好的
continue;
}
...
case CODE_DIV:
{
Object b = stk.pop();
Object a = stk.pop();
Object c = a.div(b);
if (c == null)
{
return null;
}
stk.push(c);
}
...
case CODE_JMP: //假设采用绝对地址跳转
{
idx = inst.arg; //假设arg是个int类型,否则根据实际情况转换
continue;
}
...
case CODE_CALL:
{
Object[] func_arg = new Object[inst.arg]; //call ARG_COUNT这种形式
for (int i = inst.arg - 1; i >= 0; -- i) //弹出参数
{
func_arg[i] = stk.pop();
}
Object func = stk.pop();
Object ret = execute(func.inst_list, func_arg); //递归调用解释器解释要call的函数
if (ret == null)
{
//异常了
return null;
}
stk.push(ret); //ret是函数调用的运算结果,压栈继续计算
continue;
}
...
case CODE_RETURN:
{
//栈顶是需要返回的值
return stk.pop();
}
...
default:
{
//字节码非法,严重错误,直接退出整个解释器
show_fatal_error("Invalid instruction");
exit(1);
}
}
//这里不需要返回值,因为上面是死循环
}
上面这个大结构已经能表示几乎所有机制了,像pop_jmp_if_false之类的代码就没有列出来,但很容易也能想到怎么实现的,单个字节码的操作都是非常简单的
需要注意上面的ADD和DIV两个过程,ADD那个实际是错的实现,DIV的是对的,因为动态类型语言的运算可能是会有异常的,当然就伪代码来说,ADD也表述清楚意思了,DIV判断了div过程是否有异常,比如整数对象的div过程的实现可能是:
Object div(Object x)
{
if (!(x instanceof IntObj))
{
env.set_exception("Int div by non-Int");
return null;
}
IntObj i = (IntObj)x;
if (i.is_zero())
{
//除数为0异常
env.set_exception("Zero division");
return null;
}
return new IntObj(this.value / i.value);
}
函数调用方便起见直接使用了解释器本身实现语言的递归,也可以实现为先压栈再jmp代码,这样只需要一个execute就可以执行一个很复杂的程序,当然需要实现数据栈的压栈操作,就像汇编一样,两种做法差别不算很大,后面只讨论上面的递归做法
最后是异常机制的实现,这个上面只体现了抛出异常,而没有捕获的代码,相关字节码可能是这样:
setup_try ON_EXC
...
@ON_EXC
...
实现的时候,需要一个try_stk保持当前函数的try栈(因为try可以嵌套),然后在出现异常的时候:
if (c == null)
{
if (try_stk.size() > 0)
{
//有try,跳到捕获错误的代码位置
idx = try_stk.pop();
continue;
}
return null; //当前函数没有try,抛给上一层
}
当然,如果宿主语言本身支持异常机制,如java,则可以利用throw来抛异常,不过,这个异常体系最好不要和宿主的混起来,自己设计一套比较好