此常见问题解答包含许多在 IRC 和邮件列表中提出的问题的答案。
请参阅 Emscripten 教程 和 emcc.
在 Emscripten 测试套件 中的所有测试都在我们的测试基础设施上已知构建并通过,因此如果您在本地看到失败,则可能是您的环境存在一些问题。(很少,可能会有暂时的故障,但永远不会出现在标记的发布版本上。)
首先调用 emcc --check
,它将运行基本的健全性检查并打印出有用的环境信息。如果这没有帮助,请按照 验证 Emscripten 开发环境 中的说明操作。
您可能还希望再次浏览 Emscripten 教程,因为它会随着 Emscripten 的更改而更新。
另外,请确保您具有运行 Emscripten 所需的必要要求,如 SDK 部分中所述,包括最新版本的依赖项。
一些可能有助于弄清楚问题的一般步骤
查看问题是否在没有优化 (-O0 或未指定任何优化级别) 的情况下发生。没有优化,emscripten 在编译和运行时启用许多断言,这可能会捕获问题并显示一条带有修复建议的错误消息。
搜索此网站上的文档。
检查 Emscripten 测试套件 中是否有针对失败功能的测试(在 **test/** 中运行
grep -r
)。它们都应该通过(只有很少的例外),因此它们提供了关于如何使用各种选项和代码的具体“已知良好”示例。
在大多数情况下,您将能够在 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
一起使用,可能会出现问题。
确保您正在运行 优化构建(更小的构建启动速度更快)。
网络延迟也是启动时间的一个可能因素。考虑将文件加载代码放在与生成代码分开的脚本元素中,以便浏览器可以并行启动网络下载以启动代码库(运行 文件打包程序 并将文件加载代码放在一个脚本元素中,并将生成的代码库放在后面的脚本元素中)。
当使用 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 wasm32
或 unknown 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 页面挂起的常见原因是 JavaScript 无法完成并返回控制权给浏览器。
图形 C++ 应用程序通常有一个无限的主循环,其中事件处理、处理和渲染完成,然后是延迟以保持正确的帧速率 (SDL_DELAY
在 SDL 应用程序中)。由于主循环不会完成(是无限的),它不能将控制权返回给浏览器,并且应用程序将挂起。
使用无限主循环的应用程序应该重新编码,将循环单次迭代的操作放入单个“有限”函数中。在原生构建中,该函数可以像以前一样在无限循环中运行。在 Emscripten 构建中,它被设置为 主循环函数,并且将由浏览器以指定频率调用。
有关此主题的更多信息,请参阅 Emscripten 运行时环境。
要反复运行 C 函数,请使用 emscripten_set_main_loop()
(这在 Emscripten 运行时环境 中有讨论)。emscripten.h 中的相关函数也很有用,允许您添加阻塞主循环的事件等。
要响应浏览器事件,请以正常方式使用 SDL API。SDL 测试中有一些示例(在 test/runner.py 中搜索 SDL)。
另请参阅:为什么我的 HTML 应用程序挂起?
请参阅 SDL 自动测试以获取工作示例:test/runner.py browser
。
编译时会自动链接包含在 Emscripten 中的系统库(仅必要部分)。这包括 libc、libc++(C++ 标准库)和 SDL。
未包含在 Emscripten 中的库(如 Boost)必须与程序一起编译和链接,就像它们是项目中的模块一样。
有一组移植到 Emscripten 的库,以便于使用,即 Emscripten 端口。请参阅 构建项目
另一个选择是将所需的 C API 实现为 JavaScript 库(请参阅 emcc 中的 --js-library
和 在 JavaScript 中实现 C API)。Emscripten 本身就是为 libc(不包括 malloc)和 SDL(但不包括 libc++ 或 malloc)执行此操作的。
注意
与其他编译器不同,您不需要 -lSDL
来包含 SDL(指定它不会造成任何伤害)。
在 Boost 的特定情况下,如果您只需要 boost 头文件,则无需编译任何内容。
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 set
或 stdio 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
。
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_FUNCTIONS
或 EMSCRIPTEN_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 Compiler 将缩小文件服务器 API 代码。使用文件系统的代码必须使用 emcc 的 --pre-js
选项 与文件系统 API 一起优化。
-O2 --closure 1
时会崩溃并出现奇怪的错误?¶Closure Compiler 会缩小变量名,这会导致出现非常短的变量名,如 i
、j
、xa
等。如果其他代码在全局作用域中声明了相同名称的变量,则会导致严重问题。
如果你可以成功运行使用 -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
选项时,我会收到 NameError
或 a 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
是一个纯文本文件,包含 foo
和 bar
,分别位于不同的行。
-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
的参数是字符串列表,因此它接受 a
或 a,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
。
如果您使用 -sWASM_BIGINT 标志进行构建,那么 int64_t 和 uint64_t 将在 JS 中表示为 bigint 值。如果没有 -sWASM_BIGINT 标志,这些值将在 JS 中表示为 number,而 number 无法表示 int64,因此发生的情况是在导出函数(您可以从 JS 调用)中,我们会通过将 i64 参数转换为两个 i32(低位和高位)来“合法化”类型,而 i64 返回值会变成一个 i32,您可以通过调用名为 getTempRet0 的辅助函数来访问高位。
默认情况下,Emscripten 输出只是一些代码。放在 script 标签中意味着代码在全局范围内。因此,同一页面上的多个这样的模块无法工作。
但是,通过将每个模块放在函数范围内,可以避免这个问题。Emscripten 甚至为此提供了一个编译标志 MODULARIZE
,它与 EXPORT_NAME
结合使用非常有用(有关详细信息,请参阅 settings.js)。
但是,如果相同的 Module 对象(定义画布、文本输出区域等)在不同的模块之间使用,仍然会有一些问题。默认情况下,Emscripten 输出甚至会在全局范围内查找 Module,但当使用 MODULARIZE
时,您会得到一个必须用 Module 作为参数调用的函数,因此避免了这个问题。但请注意,每个模块可能都需要自己的画布、文本输出区域等;仅传递相同的 Module 对象(例如,来自默认的 HTML shell)可能不起作用。
因此,通过使用 MODULARIZE
并为每个模块创建适当的 Module 对象,并将它们传递进去,多个模块可以正常工作。
另一种选择是使用 iframe,在这种情况下,默认的 HTML shell 就可以工作,因为每个 iframe 都有自己的画布等。但这对于可以如上所述以模块化方式运行的小型程序来说过于复杂。
是的,您可以在 settings.js
中使用 ENVIRONMENT 选项。例如,使用 emcc -sENVIRONMENT=web
构建将发出只在 Web 上运行的代码,并且不包含对 Node.js 和其他环境的支持代码。
这有助于减小代码大小,并且可以解决 Node.js 支持代码使用 require()
的问题,Webpack 会处理它并包含不必要的代码。
我不知道为什么;这是一个完全 cromulent 的词!