# 运行时

AssemblyScript 运行时实现了内存管理和垃圾收集所需的必要部分。它在很大程度上对开发人员不可见,但在高级用例中可能变得相关,例如在将 AssemblyScript 集成到新环境中时。

提示

如果与您的用例无关,即不需要直接与运行时交互,您可以放心地跳过此部分。

# 变体

运行时有不同的形式,每种形式在不同的用例中都有用。可以使用 --runtime 编译器选项指定所需的运行时。

  --runtime             Specifies the runtime variant to include in the program.

                         incremental  TLSF + incremental GC (default)
                         minimal      TLSF + lightweight GC invoked externally
                         stub         Minimal runtime stub (never frees)
                         ...          Path to a custom runtime implementation

  --exportRuntime       Exports the runtime helpers (__new, __collect etc.).

默认的 incremental 运行时提供了大多数用例中推荐的完整包。minimal 运行时是一个简化的变体(没有影子堆栈,没有启发式方法,更简单的算法,更小,更高效),它不是自动化的,需要在适当的时候(当 WebAssembly 堆栈上没有更多值时,这种情况发生在 WebAssembly 直接或间接地调用宿主时)在外部调用 __collect,而 stub 运行时根本不提供垃圾收集器,并且永远不会释放(简单的增量分配,非常小)。

例如,stub 运行时在模块的生命周期很短并且作为一个整体进行收集的情况下很有用,而 minimal 运行时为那些只需要偶尔手动收集垃圾的用例提供了折衷方案,例如一个模块在再次调用之前执行固定数量的工作。

如有疑问,请使用 incremental,但请随意尝试其他可能更高效的变体,只要用例允许。

# 接口

使用 --exportRuntime 编译器选项,可以从模块导出运行时接口到宿主,这样就可以在外部分配新的托管对象并调用垃圾收集器。

通常不需要手动调用运行时接口,因为生成的绑定会处理所有内部细节。但是,在没有绑定可用的环境中,可以使用运行时接口来提供必要的集成。

  • function __new(size: usize, id: u32): usize
    

    分配由指定类 ID 表示的、至少为指定大小的对象的新的垃圾回收实例。返回指向该对象的指针(指向其数据,而不是其内部头)。

  • function __pin(ptr: usize): usize
    

    ptr 指向的对象在外部固定,这样它和它引用的任何对象都不会被垃圾收集。请注意,同一个对象不能被固定多次。

    从 WebAssembly 内部未引用的外部对象,必须在分配它和将其传递给 WebAssembly 之间可能会发生分配时固定。如果没有固定,分配可能会触发垃圾收集器进行步骤,这将过早地收集该对象,并导致未定义的行为。

  • function __unpin(ptr: usize): void
    

    ptr 指向的对象在外部取消固定,这样它就可以被垃圾收集。请注意,相应的对象必须在取消固定操作才能成功之前被固定。

  • function __collect(): void
    

    执行完整的垃圾收集。

  • const __rtti_base: usize
    

    指向线性内存中运行时类型信息的指针。RTTI 包含有关二进制文件中使用的各种类的信息,将类 ID 映射到对象类型、它们的键和值布局以及基类。它的布局在 shared/typeinfo (在新窗口中打开) 中进行了详细描述。使用 RTTI,就可以实现 instanceof,例如,或者区分字符串、数组等。

# 内存布局

总的来说,AssemblyScript 将线性内存划分为以下部分:

区域 起始偏移量 结束偏移量 描述
静态数据 0 __data_end 静态字符串、数组等的內容。
托管堆栈 __data_end __heap_base 仅当使用增量运行时时才存在。
__heap_base memory.size() << 16 剩余空间用于动态分配。可以增长。

# 头布局

AssemblyScript 中的任何类型的托管对象都使用托管头,以便运行时对其进行操作。

名称 偏移量 类型 描述
mmInfo -20 usize 内存管理器信息
gcInfo -16 usize 垃圾收集器信息
gcInfo2 -12 usize 垃圾收集器信息
rtId -8 u32 具体类的唯一 ID
rtSize -4 u32 紧随其后的数据的大小
0 有效载荷从这里开始

对对象的引用始终指向有效载荷的开头,头位于之前 20 个字节处。空引用只是值 0。在外部使用 AssemblyScript 模块时,了解内存布局可能有助于例如获取对象的类 ID 或大小。调用 __new 会自动添加托管头,并使用提供的类 ID 作为 rtId 和提供的大小作为 rtSize 来向 GC 注册对象。

# 类布局

类字段的布局类似于 C 结构体,按顺序排列,不进行打包。每个字段都与其类型的本机对齐方式对齐,在字段之间可能会留下填充。如果一个类具有 @unmanaged 装饰器,它实际上只描述一个内存区域,就像它是一个不使用托管头的结构一样,不会被垃圾收集,并且可以使用 heap.free。具有托管头的托管类不能手动释放,托管类和非托管类不能混合使用(例如,相互继承)。

标准库数据类型使用以下布局:

描述
对象 对象,所有托管类的基类,其类 ID 为 0
ArrayBuffer 缓冲区始终使用类 ID 1,其非类型化数据作为有效载荷。
字符串 字符串始终使用类 ID 2,其 16 位字符代码(UTF-16 代码单元,允许使用像 JS 一样的孤立代理)作为有效载荷。例如,如果 rtSize8,则字符串的 .length4
TypedArray 类型化数组是对象,由 buffer(指向所查看的 ArrayBuffer 的引用)、dataStart(指向 buffer 中的起始指针)和 byteLength 字段组成,按此顺序排列。相应的 ID 是按顺序选择的,而不是预先确定的。
Array<T> 普通数组与类型化数组使用相同的布局,最后一个字段是一个可变的 length 字段。
StaticArray<T> 静态数组不需要间接寻址,因为它们不可调整大小,并且它们的数据直接位于有效载荷中,根据 T 对齐。可以认为是类型化的缓冲区。
函数 函数是对象,由其表 index 和其词法 env(当前始终为 0)组成。
Map/Set/... 其他集合使用更复杂的布局。请参阅它们各自的源代码。

还可以构建自定义数据类型,通过实现 __visit 接口(迭代这种类型的对象中包含的所有引用)与 GC 集成。请参阅现有数据类型的源代码以了解示例。

# 调用约定

AssemblyScript 的调用约定比较简单,因为它不会向函数或其他幕后魔法添加额外的参数。请注意,生成的绑定会自动处理调用约定,但其他环境可能希望专门遵守它。

# 基本值

导出的函数将基本数字返回值包装到其各自的值范围内。传递给导出的函数的基本数字值根据需要被包装到其各自的值范围内。

# 托管值

任何类型的对象都以指针的形式传递到内存中,主机需要执行必要的步骤来交换线性内存和主机系统之间的值。例如,当字符串从 WebAssembly 传递到主机或反之亦然时,传递的不是字符串本身的数据,而是指向线性内存中字符串数据的指针。为了读取数据,可以评估有效载荷之前的托管标头以获取字符串的字节长度。请注意,主机绑定 会自动执行此过程以处理常见的数据类型。

# 可选参数

当导出的函数允许省略一个或多个参数时,模块必须通过在调用导出之前调用 exports.__setArgumentsLength(numArgs) 来告知模块有效参数的数量,以便它可以填写默认值。省略的参数不会被评估,也就是说可以传递零。不调用此辅助函数会导致未定义的行为。如果函数具有固定数量的参数,则不需要调用此辅助函数。如果模块只导出具有固定数量参数的函数,则可能不存在此辅助函数。