常见问题解答

此常见问题解答包含许多在 IRC 和邮件列表中提出的问题的答案。

如何编译代码?

请参阅 Emscripten 教程emcc.

为什么我在构建基本代码和测试时遇到错误?

Emscripten 测试套件 中的所有测试都在我们的测试基础设施上已知构建并通过,因此如果您在本地看到失败,则可能是您的环境存在一些问题。(很少,可能会有暂时的故障,但永远不会出现在标记的发布版本上。)

首先调用 emcc --check,它将运行基本的健全性检查并打印出有用的环境信息。如果这没有帮助,请按照 验证 Emscripten 开发环境 中的说明操作。

您可能还希望再次浏览 Emscripten 教程,因为它会随着 Emscripten 的更改而更新。

另外,请确保您具有运行 Emscripten 所需的必要要求,如 SDK 部分中所述,包括最新版本的依赖项。

我尝试了一些东西:为什么它不起作用?

一些可能有助于弄清楚问题的一般步骤

  • 查看问题是否在没有优化 (-O0 或未指定任何优化级别) 的情况下发生。没有优化,emscripten 在编译和运行时启用许多断言,这可能会捕获问题并显示一条带有修复建议的错误消息。

  • 搜索此网站上的文档。

  • 检查 Emscripten 测试套件 中是否有针对失败功能的测试(在 **test/** 中运行 grep -r)。它们都应该通过(只有很少的例外),因此它们提供了关于如何使用各种选项和代码的具体“已知良好”示例。

我是否需要更改我的构建系统才能使用 Emscripten?

在大多数情况下,您将能够在 Emscripten 中使用项目的当前构建系统。请参阅 构建项目.

为什么代码编译很慢?

Emscripten 做了一些权衡,以使生成的代码更快更小,但代价是链接时间更长。例如,我们使用 -flto(链接时优化)构建标准库的某些部分,这将启用一些额外的优化,但构建时间可能更长。我们还在(在优化构建中)对整个输出运行 Binaryen 优化器,即使没有 LTO。

注意

您可以通过在环境中使用 EMCC_DEBUG=1 编译,然后查看调试日志(默认情况下在 /tmp/emscripten_temp 中)来确定哪些编译步骤花费时间最长。请注意,在调试模式下编译比正常情况花费的时间更长,因为我们将许多中间步骤打印到磁盘,因此它对于调试很有用,但不适用于实际编译。

提高构建时间的几个主要技巧:

  • 对于快速迭代构建,使用 -O0。您仍然可以使用更高的优化级别进行编译,但在链接期间指定 -O0 将使链接步骤快得多。

  • 在具有更多内核的机器上编译

    • 对于编译源文件,请使用并行构建系统(例如,在 make 中,您可以执行类似 make -j8 的操作以使用 8 个内核运行)。

    • 对于链接步骤,Emscripten 可以并行运行一些优化(具体而言,Wasm 的 Binaryen 优化以及我们的 JavaScript 优化)。增加内核数量会导致几乎线性的改进。Emscripten 会自动使用更多内核(如果可用),但您可以使用环境中的 EMCC_CORES=N 来控制它(如果您有许多内核但内存相对较少,这将很有用)。

为什么我的代码运行缓慢?

确保您通过使用 -O2 构建来优化代码(甚至更 积极的优化 可用,但代价是编译时间显着增加)。

为什么我编译的代码很大?

确保您使用 -O3-Os 构建,以便代码完全优化和缩小。您应该使用闭包编译器、在 Web 服务器上进行 gzip 压缩等,请参阅 优化代码部分中的代码大小部分.

为什么编译在其他机器上工作的代码会导致错误?

确保您使用的是 Emscripten 捆绑的系统头文件。使用 emcc 将默认执行此操作,但如果您使用本地系统头文件与 emcc 一起使用,可能会出现问题。

如何缩短启动时间?

确保您正在运行 优化构建(更小的构建启动速度更快)。

网络延迟也是启动时间的一个可能因素。考虑将文件加载代码放在与生成代码分开的脚本元素中,以便浏览器可以并行启动网络下载以启动代码库(运行 文件打包程序 并将文件加载代码放在一个脚本元素中,并将生成的代码库放在后面的脚本元素中)。

如何运行本地 Web 服务器以进行测试 / 为什么我的程序停滞在“正在下载...”或“正在准备...”中?

当使用 file:// URL 加载页面时,可能会发生该错误,该错误在某些浏览器中有效,但在其他浏览器中无效。相反,最好使用本地 Web 服务器。例如,Python 具有一个内置的 Web 服务器,Python 3 中为 python -m http.server 或 Python 2 中为 python -m SimpleHTTPServer。执行完此操作后,您可以访问 https://127.0.0.1:8000/。您也可以使用 emrun FILENAME.html(它将为您运行 python Web 服务器)。

在进行快速本地测试时,除了本地 Web 服务器之外,另一个选择是使用 -sSINGLE_FILE 将所有内容捆绑到单个文件中(因为这样就不会对 file:// URL 进行任何 XHR)。

否则,要调试此问题,请查看页面本身报告的错误,或查看浏览器开发者工具(Web 控制台和网络选项卡),或查看 Web 服务器的日志。

为什么我在链接期间收到 machine type must be wasm32unknown file type

这意味着一个或多个链接器输入文件不是由 Emscripten 构建的(或者更具体地说,不是为正确的目标架构构建的)。

最常见的是,该文件将是为主机机器构建的 ELF 文件或 Mach-O 文件。您可以运行 file 命令行实用程序以查看它们实际包含的内容。

常见问题包括:

  • 尝试链接针对主机系统构建的库。例如,如果您在链接命令中包含类似于 -L/usr/lib 的内容,则几乎总是会导致这些错误,因为这些系统目录中存在的库几乎肯定不是使用/为 Emscripten 构建的。解决方法是使用 Emscripten 构建所有依赖的库,并且永远不要使用主机库。

  • 项目中的一些库或目标文件是使用主机编译器而不是 emscripten 编译器构建的。如果您使用的是 autoconf 或 cmake,请确保使用 emconfigure/emmake 包装器,请参阅 构建项目

  • 来自旧后端的 LLVM IR,如果您使用的是 1.39.0 之前的版本(默认情况下使用旧后端)构建了项目,并且现在正在进行增量重建。要解决此问题,请从头开始完全重建项目的所有文件,包括库(此错误通常发生在您有来自第三方的预构建库的情况下;这些库也必须使用新后端重新编译)。

为什么我的代码在有关内联汇编(或 {"text":"asm"})的错误消息下无法编译?

Emscripten 无法编译内联汇编代码(除非该汇编代码专门针对 WebAssembly 编写)。

您需要找到使用内联汇编的位置,并将其禁用或替换为平台无关的代码。

为什么我的 HTML 应用程序挂起?

浏览器事件模型使用协作式多任务 - 每个事件都有一个“轮次”来运行,然后必须将控制权返回给浏览器事件循环,以便处理其他事件。HTML 页面挂起的常见原因是 JavaScript 无法完成并返回控制权给浏览器。

图形 C++ 应用程序通常有一个无限的主循环,其中事件处理、处理和渲染完成,然后是延迟以保持正确的帧速率 (SDL_DELAYSDL 应用程序中)。由于主循环不会完成(是无限的),它不能将控制权返回给浏览器,并且应用程序将挂起。

使用无限主循环的应用程序应该重新编码,将循环单次迭代的操作放入单个“有限”函数中。在原生构建中,该函数可以像以前一样在无限循环中运行。在 Emscripten 构建中,它被设置为 主循环函数,并且将由浏览器以指定频率调用。

有关此主题的更多信息,请参阅 Emscripten 运行时环境

如何运行事件循环?

要反复运行 C 函数,请使用 emscripten_set_main_loop()(这在 Emscripten 运行时环境 中有讨论)。emscripten.h 中的相关函数也很有用,允许您添加阻塞主循环的事件等。

要响应浏览器事件,请以正常方式使用 SDL API。SDL 测试中有一些示例(在 test/runner.py 中搜索 SDL)。

另请参阅:为什么我的 HTML 应用程序挂起?

为什么我的 SDL 应用程序无法正常工作?

请参阅 SDL 自动测试以获取工作示例:test/runner.py browser

我的音频播放选项有哪些?

Emscripten 部分支持 SDL1 和 2 音频以及 OpenAL。

要使用 SDL1 音频,请将其包含为 #include <SDL/SDL_mixer.h>。您可以将它与 SDL1、SDL2 或其他库一起使用,以进行平台集成。

要使用 SDL2 音频,请将其包含为 #include <SDL2/SDL_mixer.h> 并使用 -sUSE_SDL_MIXER=2。当前格式支持仅限于 OGG、WAV、MID 和 MOD。

我的编译程序如何访问文件?

Emscripten 使用虚拟文件系统,该系统可以预加载数据或链接到 URL 以进行延迟加载。有关更多详细信息,请参阅 文件系统概述

为什么我的代码无法访问同一目录中的文件?

浏览器中运行的 Emscripten 生成的代码无法访问本地文件系统中的文件。相反,您可以使用 预加载嵌入 来解决缺乏同步文件 IO 的问题。有关更多信息,请参阅 文件系统概述

可以允许在node.js 中运行的代码访问本地文件系统,使用 NODEFS 文件系统选项。

如何判断页面何时完全加载以及何时可以安全地调用已编译的函数?

(如果您看到类似于 native function `x` called before runtime initialization 的错误消息,则可能需要此答案,这是 ASSERTIONS 构建中启用的检查。)

在页面完全加载之前调用已编译的函数可能会导致错误,如果该函数依赖于可能不存在的文件(例如,预加载 文件是异步加载的,因此,如果您只是在 --post-js 中放置一些调用已编译代码的 JS,该代码将在组合的 JS 文件末尾同步调用,可能在异步事件发生之前,这是不好的)。

找出加载何时完成的最简单方法是添加一个 main() 函数,并在其中调用 JavaScript 函数以通知您的代码加载已完成。

注意

main() 函数在启动完成之后被调用,作为可以安全地调用任何已编译方法的信号。

例如,如果 allReady() 是您希望在一切准备就绪时调用的 JavaScript 函数,您可以执行以下操作

#include <emscripten.h>

int main() {
  EM_ASM( allReady() );
}

另一个选择是定义一个 onRuntimeInitialized 函数,

Module['onRuntimeInitialized'] = function() { ... };

该方法将在运行时准备就绪且可以安全地调用已编译代码时被调用。实际上,这与调用 main() 的时间完全相同,因此 onRuntimeInitialized 不会让您做任何新事情,但您可以以灵活的方式在运行时从 JavaScript 设置它。

以下是如何使用它的示例

<script type="text/javascript">
  var Module = {
    onRuntimeInitialized: function() {
      Module._foobar(); // foobar was exported
    }
  };
</script>
<script type="text/javascript" src="my_project.js"></script>

关键是 Module 存在,并且具有属性 onRuntimeInitialized,在包含 emscripten 输出的脚本(在本例中为 my_project.js)加载之前。

另一个选择是使用 MODULARIZE 选项,使用 -sMODULARIZE。这将所有生成的 JavaScript 放入工厂函数中,您可以调用该函数以创建模块的实例。工厂函数返回一个 Promise,该 Promise 使用模块实例解析。一旦可以安全地调用已编译的代码(即已编译的代码已下载并实例化后),该 Promise 就会被解析。例如,如果您使用 -sMODULARIZE -s 'EXPORT_NAME="createMyModule"' 构建,那么您可以执行以下操作

createMyModule(/* optional default settings */).then(function(Module) {
  // this is reached when everything is ready, and you can call methods on Module
});

请注意,在 MODULARIZE 模式下,我们不会为默认值查找全局 Module 对象。默认值必须作为参数传递给工厂函数。(有关详细信息,请参阅 settings.js)

“退出运行时”是什么意思?为什么 atexit()s 不会运行?

(如果您看到类似于 atexit() called, but EXIT_RUNTIME is not setstdio streams had content in them that was not flushed. you should set EXIT_RUNTIME to 1 的错误消息,则可能需要此答案。)

默认情况下,Emscripten 设置 EXIT_RUNTIME=0,这意味着我们不包含关闭运行时的代码。这意味着当 main() 退出时,我们不会刷新 stdio 流,也不会调用全局 C++ 对象的析构函数,也不会调用 atexit 回调。这让我们可以默认生成更小的代码,通常这也是你在网页上想要的:即使 main() 退出,你可能还有其他异步操作会在之后执行。

但是,在某些情况下,你可能想要更像“命令行”的体验,即在 main() 退出时关闭运行时。你可以使用 -sEXIT_RUNTIME 构建,然后我们将调用 atexits 等。当你使用 ASSERTIONS 构建时,如果需要使用此选项,你应该会收到警告。例如,如果你的程序在没有换行符的情况下打印了一些内容,

#include <stdio.h>

int main() {
  printf("hello"); // note no newline
}

如果我们不关闭运行时并刷新 stdio 流,“hello” 不会被打印。在 ASSERTIONS 构建中,你会收到一条通知,提示 stdio streams had content in them that was not flushed. you should set EXIT_RUNTIME to 1

为什么当我编译为 WebAssembly 时,C/C++ 源代码中的函数会消失?

Emscripten 会对未从编译代码中调用的函数进行死代码消除。虽然这确实可以最小化代码大小,但它可能会删除你计划自己调用的函数(在编译代码之外)。

为了确保 C 函数可以从正常的 JavaScript 中调用,它必须添加到 EXPORTED_FUNCTIONS 中,使用 emcc 命令行。例如,要防止函数 my_func()main() 被删除/重命名,请使用以下命令运行 emcc

emcc -sEXPORTED_FUNCTIONS=_main,_my_func  ...

注意

如果你有 main() 函数,那么 _main 应该包含在导出列表中,就像示例中一样。否则,它将被视为死代码删除;默认情况下,没有特殊的逻辑来保留 main()

注意

EXPORTED_FUNCTIONS 会影响编译为 JavaScript 的过程。如果你先编译为目标文件,然后再编译目标文件为 JavaScript,则需要在第二个命令中使用该选项。

如果你的函数在其他函数中使用,LLVM 可能会对其进行内联,并且它在 JavaScript 中将不会显示为唯一的函数。可以通过使用 EMSCRIPTEN_KEEPALIVE 定义函数来防止内联。

void EMSCRIPTEN_KEEPALIVE yourCfunc() {..}

EMSCRIPTEN_KEEPALIVE 也会导出该函数,就好像它在 EXPORTED_FUNCTIONS 中一样。

注意

  • 所有未通过 EXPORTED_FUNCTIONSEMSCRIPTEN_KEEPALIVE 保持活动状态的函数,都有可能被删除。请确保使用上述一种或两种方法来保留你需要保留的内容。

  • 导出的函数必须是 C 函数(以避免 C++ 名称重整)。

  • 如果你不想显式跟踪要导出的函数,并且这些导出不会改变,那么用 EMSCRIPTEN_KEEPALIVE 装饰你的代码可能很有用。它并不一定适用于从其他库导出函数 - 例如,对 C 标准库的源代码进行装饰和重新编译并不是一个好主意。如果你以多种方式构建相同的源代码,并且更改了导出的内容,那么在命令行上管理导出会更容易。

  • 使用 -sLINKABLE 运行 emcc 也会禁用链接时优化和死代码消除。不建议这样做,因为它会使代码更大,优化程度更低。

代码丢失的另一个可能原因是 .a 文件的链接不正确。 .a 文件只链接之前命令行中文件所需的内部目标文件,因此文件的顺序很重要,这一点可能会让人感到意外。如果你要链接 .a 文件,请确保它们位于文件列表的末尾,并且在它们自身之间处于正确的顺序。或者,在你的项目中直接使用 .so 文件。

提示

使用 EMCC_DEBUG=1 设置环境(在 Linux 上使用 EMCC_DEBUG=1 emcc ...,在 Windows 上使用 set EMCC_DEBUG=1)可能会很有用。这将拆分编译步骤,并将它们保存在 /tmp/emscripten_temp 中。然后你可以看到代码在哪个阶段消失了(你需要在 bitcode 阶段对它们执行 llvm-dis 以便读取它们,或者执行 llvm-nm 等)。

为什么当我使用 closure 构建时,文件系统 API 不可使用?

Closure Compiler 将缩小文件服务器 API 代码。使用文件系统的代码必须使用 emcc 的 --pre-js 选项 与文件系统 API 一起优化。

为什么我的代码在使用 -O2 --closure 1 时会崩溃并出现奇怪的错误?

Closure Compiler 会缩小变量名,这会导致出现非常短的变量名,如 ijxa 等。如果其他代码在全局作用域中声明了相同名称的变量,则会导致严重问题。

如果你可以成功运行使用 -O2 设置且 --closure 未设置编译的代码,那么这很可能是原因。

一种解决方案是停止在全局作用域中使用简短的变量名(这通常是一个错误 - 在给变量赋值时忘记使用 var)。

另一种方法是将生成的代码(或你的其他代码)包装在闭包中,如下所示:

var CompiledModule = (function() {
  .. GENERATED CODE ..
  return Module;
  })();

为什么我会收到 TypeError: Module.someThing is not a function

Module 对象将包含导出的方法。要使其出现在那里,你应该将其添加到 EXPORTED_FUNCTIONS 中(用于编译代码),或将其添加到 EXPORTED_RUNTIME_METHODS 中(用于运行时方法,如 getValue)。例如,

emcc -sEXPORTED_FUNCTIONS=_main,_my_func ...

将导出 C 方法 my_func(在本示例中还将导出 main)。并且

emcc -sEXPORTED_RUNTIME_METHODS=ccall ...

将导出 ccall。在这两种情况下,你都可以在 Module 对象上访问导出的函数。

注意

如果编译器可以识别到运行时方法的使用,你也可以直接使用它们,而无需导出它们。例如,你可以在 EM_ASM 代码或 --pre-js 中直接调用 getValue。优化器不会删除该 JS 运行时方法,因为它识别到它被使用。你只需要在想要从编译器无法识别到的 JS 代码外部调用该方法时才使用 Module.getValue,并且你需要导出它。

注意

Emscripten 曾经默认导出许多运行时方法。这增加了代码大小,因此我们改变了默认设置。如果你依赖于曾经被导出的东西,你应该会看到一个警告,指引你找到解决方案,在非优化构建中,或者在启用了 ASSERTIONS 的构建中,我们希望这可以最大程度地减少任何困扰。有关详细信息,请参阅 ChangeLog.md

为什么 Runtime 不再存在?为什么当我尝试访问 Runtime.someThing 时会收到错误?

1.37.27 包含一个重构,用于移除 Runtime 对象。这使生成的代码更高效,更紧凑,但如果你使用过 Runtime.* API,则需要进行一些小的更改。你只需要移除 Runtime. 前缀,因为这些函数现在是顶级作用域中的简单函数(在 -O0 或启用了断言的构建中,错误消息会建议你这样做)。换句话说,将

x = Runtime.stackAlloc(10);

替换为

x = stackAlloc(10);

注意

上述方法适用于 --pre-js 或 JS 库中的代码,即与 emscripten 输出一起编译的代码。如果你尝试从编译代码之外访问 Runtime.* 方法,则必须导出该函数(使用 EXPORTED_RUNTIME_METHODS),并在 Module 对象上使用它,请参阅 该 FAQ 条目

为什么当我使用 -s 选项时,我会收到 NameErrora problem occurred in evaluating content after a -s

如果你在 -s 参数中包含非平凡的字符串,并且在获取 shell 引号/转义方面遇到困难,则可能会发生这种情况。

使用更简单的列表形式(没有引号、空格或方括号)有时会有所帮助

emcc a.c -sEXPORTED_RUNTIME_METHODS=foo,bar

你也可以使用 **响应文件**,即

emcc a.c -sEXPORTED_RUNTIME_METHODS=@extra.txt

其中 extra.txt 是一个纯文本文件,包含 foobar,分别位于不同的行。

如何在 CMake 项目中指定 -s 选项?

像这样的简单事情应该在 CMakeLists.txt 文件中正常工作。

set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -sUSE_SDL=2")

但是,一些 -s 选项可能需要加引号,或者 -s 和下一个参数之间的空格可能会在使用 target_link_options 等时混淆 CMake。为了避免这些问题,您可以使用 -sX=Y 符号,即没有空格,也没有方括号或引号。

# same as before but no space after -s
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -sUSE_SDL=2")
# example of target_link_options with a list of names
target_link_options(example PRIVATE "-sEXPORTED_FUNCTIONS=_main")

还要注意,即使 _main 是一个字符串名称,也不需要加引号(emcc 知道 EXPORTED_FUNCTIONS 的参数是字符串列表,因此它接受 aa,b 等)。

为什么我在 file=.. 或以 f'..' 开头的字符串上收到 Python SyntaxError: invalid syntax

Emscripten 需要足够新的 Python 版本。旧版本的 Python,如 2.*,默认情况下不支持 print 语句,因此它会对像 print('..', file=..) 这样的语法报错。旧版本的 3.* Python 可能不支持 f-strings,它看起来像 f'..'

确保您安装了足够新的 Python 版本(如 SDK 指令中所述),并且 emcc 使用它(例如,通过使用该 Python 运行 emcc.py)。

在 CI 环境中,如果默认版本不够新,您可能需要指定要使用的 Python 版本。例如,在 Netlify 上,您可以使用 PYTHON_VERSION

为什么我在优化时收到堆栈大小错误:RangeError: Maximum call stack size exceeded 或类似错误?

您可能需要增加 node.js 的堆栈大小。

在 Linux 和 Mac macOS 上,您可以在 Emscripten 编译器配置文件 (.emscripten) 中执行 NODE_JS = ['/path/to/node', '--stack_size=8192']。在 Windows 上(对于 node 版本早于 v19),您还需要 --max-stack-size=8192,并且还需要运行 editbin /stack:33554432 node.exe

如何从 js 将 int64_t 和 uint64_t 值传递到 Wasm 函数?

如果您使用 -sWASM_BIGINT 标志进行构建,那么 int64_tuint64_t 将在 JS 中表示为 bigint 值。如果没有 -sWASM_BIGINT 标志,这些值将在 JS 中表示为 number,而 number 无法表示 int64,因此发生的情况是在导出函数(您可以从 JS 调用)中,我们会通过将 i64 参数转换为两个 i32(低位和高位)来“合法化”类型,而 i64 返回值会变成一个 i32,您可以通过调用名为 getTempRet0 的辅助函数来访问高位。

我可以在一个网页上使用多个 Emscripten 编译的程序吗?

默认情况下,Emscripten 输出只是一些代码。放在 script 标签中意味着代码在全局范围内。因此,同一页面上的多个这样的模块无法工作。

但是,通过将每个模块放在函数范围内,可以避免这个问题。Emscripten 甚至为此提供了一个编译标志 MODULARIZE,它与 EXPORT_NAME 结合使用非常有用(有关详细信息,请参阅 settings.js)。

但是,如果相同的 Module 对象(定义画布、文本输出区域等)在不同的模块之间使用,仍然会有一些问题。默认情况下,Emscripten 输出甚至会在全局范围内查找 Module,但当使用 MODULARIZE 时,您会得到一个必须用 Module 作为参数调用的函数,因此避免了这个问题。但请注意,每个模块可能都需要自己的画布、文本输出区域等;仅传递相同的 Module 对象(例如,来自默认的 HTML shell)可能不起作用。

因此,通过使用 MODULARIZE 并为每个模块创建适当的 Module 对象,并将它们传递进去,多个模块可以正常工作。

另一种选择是使用 iframe,在这种情况下,默认的 HTML shell 就可以工作,因为每个 iframe 都有自己的画布等。但这对于可以如上所述以模块化方式运行的小型程序来说过于复杂。

我可以构建只在 Web 上运行的 JavaScript 吗?

是的,您可以在 settings.js 中使用 ENVIRONMENT 选项。例如,使用 emcc -sENVIRONMENT=web 构建将发出只在 Web 上运行的代码,并且不包含对 Node.js 和其他环境的支持代码。

这有助于减小代码大小,并且可以解决 Node.js 支持代码使用 require() 的问题,Webpack 会处理它并包含不必要的代码。

为什么项目名称这么奇怪?

我不知道为什么;这是一个完全 cromulent 的词!