WebAssembly 通过拥有多个实例化模块共享函数线性存储器、和constants使用模块imports和exports,tables在 MVP 中启用加载时和运行时( dlopen
)动态链接。特别是,由于模块可以访问的所有(非本地)状态都可以导入和导出,从而在单独的模块实例之间共享,因此工具链具有实现动态加载器的构建块。
由于加载和实例化模块的方式是由主机环境(例如JavaScript API)定义的,因此动态链接需要使用特定于主机的功能来链接两个模块。至少,宿主环境必须提供一种在将导出连接到导入时动态实例化模块的方法。
模块 A 和 B 之间最简单的加载时动态链接方案可以通过让模块 A 导出由 B 导入的函数、表和内存来实现。C++ 工具链可以通过使用当前用于从本机 DSO/DLL 导出/导入符号的相同函数属性来公开此功能:
#ifdef _WIN32
# define EXPORT __declspec(dllexport)
# define IMPORT __declspec(dllimport)
#else
# define EXPORT __attribute__ ((visibility ("default")))
# define IMPORT __attribute__ ((visibility ("default")))
#endif
typedef void (**PF)();
IMPORT PF imp();
EXPORT void exp() { (*imp())(); }
此代码至少会生成一个 WebAssembly 模块,其中包含以下内容的导入:
- 功能
imp
- 的
imp
返回值时,用于执行加载的堆。 - 用于执行指向函数调用的指针的表
和出口:
- 功能
exp
使用 libc 的更现实的模块将有更多的导入,包括:
- 线性内存中偏移量的不可变
i32
全局导入,以放置全局数据段,并在以后从全局加载和存储时用作常量基址 - 将偏移量的不可变
i32
全局导入到间接函数表中,在该表中放置模块的间接调用函数,并在以后计算其 address-of 的索引
一个额外的细节是使用模块名称什么作为导入(因为 WebAssembly 有一个两级命名空间)。一种选择是为所有 C/C++ 导入/导出使用单个默认模块名称(这允许工具链将实现内部名称放在单独的名称空间中,从而避免使用 __
前缀约定)。
要实现运行时动态链接(例如 dlopen
和 dlsym
):
dlopen
将编译并实例化一个新模块,将编译后的实例存储在主机环境表中,并将索引返回给调用者。dlsym
将被赋予这个索引,从表中拉出实例,搜索实例的导出,将找到的函数附加到函数表(在 MVP 中使用主机定义的功能,但直接来自 Future:unicorn: 中的 WebAssembly 代码),并将附加元素的表索引返回给调用者。
请注意,WebAssembly 中的 C 函数指针的表示是函数表的索引,因此上述方案与函数指针的 dlsym
返回值完全一致。
更复杂的动态链接功能(例如,插入、弱符号等)可以通过将函数表索引分配给每个弱/可变符号,通过 call_indirect
该索引调用符号,并根据需要改变底层元素来有效地模拟。
在 MVP 之后,我们希望标准化ABI每个源语言,允许 WebAssembly 库彼此接口,而不考虑编译器。指定 ABI 需要实现所有与 ABI 相关的未来功能(如 SIMD、多个返回值和异常处理)。虽然强烈建议针对 WebAssembly 的编译器遵守指定的 ABI 以实现互操作性,但 WebAssembly 运行时将与 ABI 无关,因此可以将非标准 ABI 用于特定目的。