构建到 WebAssembly

WebAssembly 是一种用于在 Web 上执行代码的二进制格式,可以实现快速启动时间(与 JS 或 asm.js 相比,下载量更小,浏览器解析速度更快)。Emscripten 默认情况下会编译到 WebAssembly,但您也可以编译到 JS 以供旧版浏览器使用。

有关一些历史背景,请参阅 这些幻灯片这篇博文

安装

默认情况下会发出 WebAssembly,无需任何特殊标志。

注意

如果您**不**想要 WebAssembly,可以使用以下方法禁用它

emcc [..args..] -sWASM=0

注意

决定编译到 Wasm 还是 JS 可以链接阶段完成:它不会影响目标文件。

后端

Emscripten 使用**上游 LLVM Wasm 后端**发出 WebAssembly,从 1.39.0 版本(2019 年 10 月)开始。以前,emscripten 也支持旧的**fastcomp** 后端,该后端在 2.0.0(2020 年 8 月)中删除了。

如果您从 fastcomp 升级到上游,您可能会注意到两个后端之间存在一些差异

  • Wasm 后端对链接具有不同功能集的文件非常严格 - 例如,如果一个文件使用原子操作构建,而另一个文件没有,则它会在链接时出错。这可以防止潜在的错误,但可能意味着您需要进行一些构建系统修复。

  • WASM=0 在两个后端中的行为不同。在 fastcomp 中,我们发出 asm.js,而在上游中,我们发出 JS(因为并非所有 Wasm 结构都可以用 asm.js 表示)。此外,JS 支持实现相同的外部 WebAssembly.* API,因此,特别是启动默认情况下将是异步的,就像 Wasm 一样,您可以使用 WASM_ASYNC_COMPILATION(即使是 WASM=0)来控制它。

  • Wasm 后端默认使用 Wasm 目标文件。这意味着它在编译步骤中进行代码生成,这使得链接步骤快得多 - 就像普通的原生编译器一样。相比之下,在 fastcomp 中,编译步骤在目标文件中发出 LLVM IR。

    • 您通常不会注意到这一点,但一些编译器标志会影响代码生成,例如 DISABLE_EXCEPTION_CATCHING。此类标志必须在代码生成期间传递。简单而安全的做法是在编译和链接时都传递所有 -s 标志。

    • 您可以使用通常的 llvm 标志(-flto-flto=full-flto=thin,在编译和链接时都使用;但是请注意,thin LTO 目前尚未经过大量测试,因此建议使用常规 LTO)。

    • 对于 fastcomp,LTO 优化传递默认情况下不会运行;为此,必须传递 --llvm-lto 1。使用 llvm 后端,LTO 传递将在任何处于位代码格式的目标文件上运行。

    • 您可能还会注意到,即使没有设置 LTO,fastcomp 的链接阶段也能够执行一些次要类型的链接时优化。LLVM 后端要求实际设置 LTO 才能执行这些操作。

  • wasm-ld 是 Wasm 后端使用的链接器,它要求库(.a 档案)包含符号索引。这与原生 GNU 链接器的行为一致。虽然 emar 默认情况下会创建此类索引,但 GNU ar 和 GNU strip 等原生工具不了解 WebAssembly 对象格式,因此无法创建档案索引。特别是,如果您在包含 WebAssembly 对象文件的档案文件上运行 GNU strip,它将删除索引,这使得档案在链接时无法使用。

  • 另请参阅有关 Wasm 后端的 阻断错误,以及 Wasm 后端标记的问题

陷井

WebAssembly 可能会陷入困境 - 抛出异常 - 在诸如除以零、将非常大的浮点数四舍五入为整数等操作上。在 asm.js 中,此类操作会被静默忽略,因为在 JavaScript 中它们不会抛出,因此这是 JavaScript 和 WebAssembly 之间的区别,您可能会注意到,浏览器会报告类似于 float unrepresentable in integer rangeinteger result unrepresentableinteger overflowOut of bounds Trunc operation 的错误。

LLVM Wasm 后端通过在每个可能的陷阱周围添加更多代码来避免陷阱(如果会发生陷阱,则基本上会对值进行钳位)。如果您不需要这些额外的代码,这会增加代码大小并降低速度。对此的正确解决方案是使用不会发生陷阱的较新 Wasm 指令,方法是在使用 -mnontrapping-fptoint 调用 emcc 或 clang 时进行调用。但是,该代码可能无法在较旧的 VM 中运行。

编译器输出

使用 emcc 构建到 WebAssembly 时,您将看到一个包含该代码的 .wasm 文件,以及通常的 .js 文件,它是编译的主要目标。这两个文件是为一起工作而构建的:运行 .js(或 .html,如果您要求这样做)文件,它将为您加载和设置 WebAssembly 代码,正确设置导入和导出等。基本上,您无需关心编译后的代码是 asm.js 还是 WebAssembly,它只是一个编译器标志,否则一切应该正常工作(除了 WebAssembly 应该更快)。

  • 请注意,.wasm 文件不是独立的 - 很难在没有该 .js 代码的情况下手动运行它,因为它依赖于获取与 JS 集成的正确导入。例如,它接收对系统调用的导入,以便它可以执行诸如打印到控制台之类的操作。目前正在进行的工作旨在创建独立的 .wasm 文件,请参阅 WebAssembly 独立页面

您可能还会看到生成的其他文件,例如如果您将文件预加载到虚拟文件系统中,则会看到 .data 文件。所有这些与构建到 asm.js 时完全相同。您可能注意到的一个区别是缺少 .mem file,该文件用于 asm.js,包含静态内存初始化数据,在 WebAssembly 中,我们可以将其更有效地打包到 WebAssembly 二进制文件本身中。

浏览器中的 WebAssembly 支持

所有主流浏览器都支持 WebAssembly,可以追溯到 Firefox 52、Chrome 57、Safari 11 和 Opera 44。

有关各种浏览器中支持的 WebAssembly 功能的更多信息,请参阅 WebAssembly 路线图

.wasm 文件和编译

WebAssembly 代码的准备方式与 asm.js 有些不同。asm.js 可以捆绑在主 JS 文件中,而如前所述,WebAssembly 是一个单独的二进制文件,因此您将有多个文件要分发。

另一个显着的效果是 WebAssembly 默认情况下是异步编译的,这意味着您必须等待编译完成才能调用编译后的代码(通过等待 main()onRuntimeInitialized 回调等,您还需要在执行任何其他导致启动异步的操作时执行此操作,例如 asm.js 的 .mem 文件或预加载的文件数据等)。您可以通过设置 WASM_ASYNC_COMPILATION=0 来关闭异步编译,但这可能无法在 Chrome 中使用,因为那里存在当前限制。

  • 请注意,即使关闭了异步编译,获取 WebAssembly 二进制文件也可能需要异步操作(因为 Web 不允许在主线程上进行同步二进制下载)。如果您能够自行获取二进制文件,则可以设置 Module['wasmBinary'],它将从那里使用,然后(在关闭异步编译的情况下)编译应该是同步的。

Web 服务器设置

为了通过网络以最有效的方式提供 Wasm,请确保您的 Web 服务器为 .wasm 文件设置了正确的 MIME 类型,即 application/wasm。这将允许流式编译,浏览器可以在下载代码时开始编译。

在 Apache 中,您可以通过以下方式实现:

AddType application/wasm .wasm

同时也要确保启用了 gzip。

AddOutputFilterByType DEFLATE application/wasm

如果您提供大型 .wasm 文件,Web 服务器会在每次请求时动态压缩它们,从而消耗 CPU 资源。您可以预先将它们压缩为 .wasm.gz,并使用内容协商。

Options Multiviews
RemoveType .gz
AddEncoding x-gzip .gz
AddType application/wasm .wasm