Berry 实现:自动扩充的调用栈

末鹿安然 提交于 2020-01-24 23:09:55

概述

调用栈用于存储函数执行过程中调用链上所有函数的局部变量等调用信息。Berry 调用栈特指脚本程序的调用栈,而不是 C 的调用栈。

在 be_vm.h 中可以看到 VM 结构中和调用栈相关的字段:

struct bvm {
    // ...
    bvalue *stack; /* stack space */
    bvalue *stacktop; /* stack top register */
    bstack callstack; /* function call stack */
    // ...
};

stackstacktop 用于维护存储局部变量的栈(以下简称“变量栈”,函数的栈空间指 vm.stack 中被该函数使用的一段空间),而 callstack 为函数栈帧的堆栈。

我们用一个简单的脚本来说明上述字段的作用:

def func1(c)
    return c + 1
end

def func2(b)
    return func1(b) + 2
end

def func3(a)
    return func2(a) + 3
end

当我们执行 func3(10) 的时候,执行到 func1 内部时调用链最长:

call stack top
+-------------------------+
|    function: func1      |
|  local variable(s): c   |
+-------------------------+
|    function: func2      |
|  local variable(s): b   |
+-------------------------+
|    function: func3      |
|  local variable(s): a   |
+-------------------------+
call stack base

很显然,调用链上所有函数的局部变量都应该被存储,以保证被调函数返回后主调函数能够继续执行。调用链上所有函数的局部变量是按照调用顺序排列的,这些变量的值都存储在 vm.stack 中。因此 vm.stack 是一个 bvalue 数组。当调用链达到最深时,各变量在 vm.stack 中的排列方式为:

stack index |    0    |    1    |    2    |
variable    | func3:a | func2:b | func1:c |  

这里要注意到 vm.stack 中存储的 3 个变量分别属于不同的函数,那么该怎样去确定每个函数在 vm.stack 中占用那些部分来存储它的局部变量呢?答案是 vm.callstack 字段,该字段为 bstack 类型,其存储元素为 bcallframe 类型。后者的定义如下:

typedef struct {
    bvalue *func; /* function register pointer */
    bvalue *top; /* top register pointer */
    bvalue *reg; /* base register pointer */
    binstruction *ip; /* instruction pointer (only berry-function) */
    int status;
} bcallframe;

该结构体用于实现函数栈帧,每个字段的功能为:

  • func:指向当前调用函数在 vm.stack 中的位置(函数在调用之前会被压入 vm.stack 中)。
  • top:该函数的栈顶指针,指向 vm.stack 的某个位置。栈顶指针总是指向函数栈空间最后一个值的后面。
  • reg:该函数的栈基址指针,指向 vm.stack 的某个位置。基址指针指向函数栈空间第一个值的位置,它总是小于等于函数栈顶。
  • ip:指令指针。VM 中也有一个当前函数的指令指针,发生函数调用时,组调函数的指令指针需要保存,因此设置该字段。
  • status:用于标记函数栈帧的一些状态。

每次发生函数调用时都要把虚拟机状态和主调函数的一些信息压入 vm.callstack 中,以便被调函数返回后能够恢复状态。这些信息包括了函数栈基址,栈顶和指令指针等。

变量栈

变量栈,也就是 vm.stack 会在 VM 创建时分配,vm.stacktop 字段指向 vm.stack 的最后一个元素,因此它包含了变量栈总容量的信息。Berry 的变量栈支持动态扩充,在 VM 刚刚创建时变量栈容量很小,而执行过程中会动态调整。栈扩充一般发生在函数调用时,解释器会检查函数需要的栈容量以决定是否扩充。如果发生扩充则执行以下流程:

  1. 重新分配变量栈并拷贝数据。
  2. 更新 vm.callstack 所有元素的的 functopreg 域。
  3. 更新所有 open upvalue 的 value 域。

其中 2、3 步中提到的结构都指向/引用了变量栈中的某个值,因此需要更新。具体的实现可以参考 be_stack_expansion() 函数的源码。

变量栈失效

变量栈失效是指变量栈扩充导致元素地址发生了变动,而指向变量栈中元素的指针却没有同时更新以致程序运行错误的现象。

Berry 的代码本身可能还存在一些变量栈失效的错误。为了避免出现该问题,我们总结了那些情况可能会导致变量栈失效:

  • Berry 函数调用。多种 API 会发生 Berry 函数调用,因此有调用栈失效的问题。
  • be_val2strbe_tostring() 等字符串转换函数可能会调用实例的 tostring 方法。
  • 触发 GC 时可能会调用析构函数,也可能发生调用栈失效。
  • 创建 GC 对象时可能触发 GC。此类操作包括创建字符串、闭包、Map 和 List 等。
  • 手动扩充变量栈的场合

以下情况不用考虑调用栈失效的问题:

  • 尽管使用 be_realloc() 接口(be_malloc() 也是由它实现)时可能触发 GC,但此时不会调用析构函数,因此不会导致调用栈失效。
  • 完全使用 Berry 的公共 API(使用 BERRY_API 修饰),这类 API 使用索引而不直接使用变量的指针,因此不必担心变量栈失效。换言之,只有 Berry 内部代码需要解决变量栈失效的问题。
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!