动态链接

注意

此文档有些过时,正在更新中。

Emscripten 支持静态链接目标文件(以及包含目标文件的 ar 档案)。这使大多数构建系统能够在几乎无需更改的情况下与 Emscripten 一起使用(请参阅 构建项目)。

此外,Emscripten 还支持一种形式的 WebAssembly 模块的动态链接。这可能会增加开销,因此为了获得最佳性能,仍然应优先考虑静态链接。但是,可以使用某些命令行标志来减少这种开销。有关详细信息,请参见下文。

背景

在我们开始谈论动态链接之前,让我们先谈谈静态链接。Emscripten 的链接模型与大多数原生平台略有不同。为了理解它,请考虑原生链接模型是在以下事实成立的情况下工作的

  1. 应用程序直接在本地系统上运行,并且可以访问本地系统库,例如 C 和 C++ 标准库,以及其他库。

  2. 代码大小不是一个大问题。部分原因是系统库已经存在于系统上,因此 C++ 中的“hello world”可以很小,即使它使用了 C++ 标准库中大量的 iostream 代码。但同时,代码大小也可能影响冷启动时间,因为更多的代码需要更长时间从磁盘加载,但这项成本通常并不显著,现代操作系统以各种方式减轻了这种成本,例如缓存他们预计要加载的应用程序。

在 Emscripten 的情况下,代码通常将在网络上运行。这意味着以下几点

  1. 应用程序在沙箱中运行。它没有本地系统库可以动态链接;它必须提供自己的系统库代码。

  2. 代码大小是一个主要问题,因为应用程序的代码正在通过互联网下载,而互联网的速度比安装在本地机器上的本地应用程序慢几个数量级。

出于这个原因,Emscripten 会自动为您处理系统库,并自动执行死代码消除等操作,以便尽其所能使其尽可能小。

这里还有一个额外的因素是 Emscripten 有“js 库” - 用 JavaScript 编写的系统库。这些系统库是我们访问网络上 API 的方式。它也是人们在同一个页面上连接编译代码和手工编写代码的便捷方式。这是 Emscripten 以特殊方式处理系统库的另一个原因,特别是以一种允许它尽可能多地剥离这些 js 库的方式,只留下实际使用的部分,并且再次,这在静态链接一个没有外部依赖关系的独立应用程序的情况下效果最好。

动态链接概述

Emscripten 的动态链接非常简单:您从源代码构建几个独立的代码“模块”,并且可以在运行时链接它们。链接基本上以最简单的方式将每个模块中的未定义符号与其他模块中的定义符号连接起来。它目前不支持一些极端情况。

系统库确实使用了一些更高级的链接功能,其中包括这些极端情况。出于这个原因,Emscripten 尝试通过以下方式简化问题:共有两种类型的共享模块

  1. **主模块**,其中链接了系统库。

  2. **边模块**,其中没有链接系统库。

一个项目应该只有一个主模块。然后它可以在运行时链接到多个边模块。此模型也使其他事情变得更简单。例如,只有单例主模块包含 JavaScript 环境,而边模块是纯 WebAssembly 模块。

这种设计的唯一棘手之处在于,边模块可能依赖于主模块没有依赖的系统库。有关如何处理这种情况,请参阅下面的系统库部分。

请注意,“主模块”不需要包含 main() 函数。它也可以位于边模块中。使主模块成为“主”模块的是,只有一个主模块,并且只有它链接了系统库。

(请注意,系统库是静态链接到主模块的。即使我们不能像我们想要的那样消除死代码,我们仍然可以从这种方式进行一些优化。)

实用细节

如果您想跳到查看运行代码,可以查看测试套件。有 test_dylink_* 测试来测试一般的动态链接,以及 test_dlfcn_* 测试来测试 dlopen() 特别是。否则,我们现在将描述该过程。

加载时动态链接

加载时动态链接是指在启动时与主模块一起加载边模块,并在应用程序开始运行之前将它们链接在一起的情况。

  • 将代码的一部分构建为主模块,使用 -sMAIN_MODULE 链接它。

  • 将代码的其他部分构建为边模块,使用 -sSIDE_MODULE 链接它。

对于主模块,输出后缀应该是 .js(WebAssembly 文件将像往常一样与它一起生成)。对于边模块,输出将只是一个 WebAssembly 模块,我们建议输出后缀为 .wasm.so(这是 UNIX 系统使用的共享库后缀)。

为了让边模块在启动时加载,您需要告诉主模块它们的存在。您可以在链接主模块时在命令行上指定它们,例如

emcc -sMAIN_MODULE main.c libsomething.wasm

在运行时,JavaScript 加载代码将在应用程序开始运行之前加载 libsomthing.wasm(以及任何其他边模块)以及主模块。然后,正在运行的应用程序可以访问链接在一起的任何模块的代码。

使用 dlopen() 的运行时动态链接

运行时动态链接可以通过调用 dlopen() 函数来完成,该函数在程序已经运行后加载边模块。该过程以相同的方式开始,使用相同的标志来构建主模块和边模块。不同之处在于,您在链接主模块时不会在命令行上指定边模块;相反,您必须将边模块加载到文件系统中,以便 dlopen(或 fopen 等)可以访问它(除了 dlopen(NULL),它表示打开当前可执行文件,这可以在没有文件系统集成的情况下正常工作)。基本上就是这样 - 您随后可以使用 dlopen(), dlsym() 等。正常。

代码大小

默认情况下,主模块会禁用死代码消除。这意味着编译的所有代码都将保留在输出中,包括链接的所有系统库,以及所有 JS 库代码。

这是默认行为,因为它最不容易让人意外。但也可以使用正常的死代码消除,方法是使用 -sMAIN_MODULE=2(而不是 1)构建。在这种模式下,主模块会像往常一样构建,没有保留代码的特殊行为。然后您有责任确保边模块需要的代码保持活动状态。您可以通过将代码添加到 EXPORTED_FUNCTIONS 中或在源代码中标记符号 EMSCRIPTEN_KEEPALIVE 来做到这一点。有关此操作的示例,请参见 other.test_minimal_dynamic

如果您正在进行加载时动态链接,那么命令行中指定的侧模块所需的任何符号将自动保持活动状态。出于这个原因,我们强烈建议在进行加载时动态链接时使用 MAIN_MODULE=2

对于侧模块,还有相应的 -sSIDE_MODULE=2

系统库

如前所述,系统库由 Emscripten 链接器以特殊方式处理,在动态链接中,只有主模块与系统库链接。在链接主模块时,可以将侧模块传递到命令行,在这种情况下,任何系统库依赖项都会自动处理。

但是,在不使用其侧模块链接主模块时(通常使用 -sMAIN_MODULE=1),可能不会包含所需的系统库。本节说明如何通过强制主模块与某些库链接来修复这个问题。

您可以在环境中使用 EMCC_FORCE_STDLIBS=1 构建主模块,以强制包含所有标准库。更精细的方法是命名要显式包含的系统库。例如,可以使用类似 EMCC_FORCE_STDLIBS=libcxx,libcxxabi 的内容(如果您需要这两个库)。

其他说明

动态检查

本机链接器通常只在所有符号都解析时才运行代码。Emscripten 的动态链接器将符号连接到这些符号的未解析引用 **动态地**。因此,我们不检查是否有任何符号未解析,即使有,代码也可以开始运行。如果实际上没有调用它们,它将成功运行。如果调用它们,您将收到运行时错误。从堆栈跟踪(在未缩小版本的构建中)可以清楚地了解出错的原因;使用 -sASSERTIONS 构建可以提供更多帮助。

局限性

  • Chromium 不支持在主线程上编译大于 4kB 的 WASM,这包括侧模块;您可以使用 --use-preload-plugins(在 emccfile_packager.py 中)使 Emscripten 在启动时编译它们 [doc] [discuss].

  • EM_ASMEM_JS 代码在侧模块中定义,取决于 eval 支持,因此与 -sDYNAMIC_EXECUTION=0 不兼容。

Pthreads 支持

动态链接 + pthreads 仍然是实验性的。因此,同时使用 MAIN_MODULE-pthread 进行链接会产生警告。

虽然加载时动态链接没有任何问题,但通过 dlopen/dlsym 进行的运行时动态链接可能需要一些额外的考虑。原因是 emscripten 库代码必须同步线程之间的间接函数指针表。每次加载新库或通过 dlsym 请求新符号时,都可以添加表格槽位,并且这些更改需要在进程中的每个线程上镜像。

对表格的更改受互斥锁保护,在任何线程从 dlopendlsym 返回之前,它将等待所有其他线程同步。为了使这种同步尽可能无缝,我们接管了 emscripten_futex_waitemscirpten_yield 的底层原语。

对于大多数用例,所有这些都在幕后发生,不需要任何特殊操作。但是,有一类应用程序目前可能需要修改。如果您的应用程序忙等待,或直接使用 atomic.waitXX 指令(或 clang 的 __builtin_wasm_memory_atomic_waitXX 内建函数),您可能需要切换到使用 emscripten_futex_wait 或避免死锁。如果您在阻塞时不使用 emscripten_futex_wait,您可能会阻塞其他正在调用 dlopen 和/或 dlsym 的线程。