构建项目

Emscripten 提供了两个脚本,它们可以配置您的 Makefile 以使用 emcc 作为 gcc 的直接替代 - 在大多数情况下,您的项目的当前构建系统将保持不变。

与构建系统集成

要使用 Emscripten 构建,您需要在您的 Makefile 中将 gcc 替换为 emcc。这可以通过使用 emconfigure 来完成,它会设置适当的环境变量,例如 CXX(C++ 编译器)和 CC(编译器)。

考虑以下情况,您通常使用以下命令进行构建

./configure
make

提示

如果您不熟悉这些构建命令,文章 configure、make、make install 背后的魔法 是一个很好的入门指南。

要使用 Emscripten 构建,您应该使用以下命令

# Run emconfigure with the normal configure command as an argument.
emconfigure ./configure

# Run emmake with the normal make to generate Wasm object files.
emmake make

# Compile the linked code generated by make to JavaScript + WebAssembly.
# 'project.o' should be replaced with the make output for your project, and
# you may need to rename it if it isn't something emcc recognizes
# (for example, it might have a different suffix like 'project.so' or
# 'project.so.1', or no suffix like just 'project' for an executable).
# If the project output is a library, you may need to add your 'main.c' file
# here as well.
# [-Ox] represents build optimisations (discussed in the next section).
emcc [-Ox] project.o -o project.js

emconfigure 接受标准的 configure 作为参数(在基于 configure 的构建系统中),而 emmake 接受 make 作为参数。如果您的构建系统使用 CMake,请在上述示例中将 ./configure 替换为 cmake . 等。如果您的构建系统不使用 configure 或 CMake,那么您可以省略第一步,直接运行 make(尽管您可能需要手动编辑 Makefile)。

提示

我们建议您在基于 configureCMake 的构建系统中调用 emconfigureemmake 脚本。您是否真的需要调用这两个工具取决于构建系统(一些系统会将环境变量存储在配置步骤中,而另一些则不会)。

Make 生成 Wasm 对象文件。它还可以将对象文件链接到库和/或 Wasm 可执行文件中。除非此类构建系统已被修改为也发出 JavaScript 输出,否则您需要运行上面所示的附加 emcc 命令,该命令将发出最终的可运行 JavaScript 和 WebAssembly。

注意

make 生成的文件输出可能具有不同的后缀:.a 表示静态库存档,.so 表示共享库,.o 表示对象文件(这些文件扩展名与 gcc 用于不同类型的文件扩展名相同)。无论文件扩展名是什么,这些文件都包含 emcc 可以编译成最终 JavaScript + WebAssembly 的内容(通常内容是 Wasm 对象文件,但如果您使用 LTO 构建,它们将包含 LLVM 位代码)。

注意

一些构建系统可能无法使用上述过程正确发出 Wasm 对象文件,您可能会看到 is not a valid input file 警告。您可以运行 file 检查文件的内容(您也可以手动检查内容是否以 \0asm 开头,以查看它们是否是 Wasm 对象文件,或者 BC 以查看它们是否是 LLVM 位代码)。运行 emmake make VERBOSE=1 也很值得,它会打印出它运行的命令 - 您应该看到使用了 emcc,而不是本机系统编译器。如果未使用 emcc,您可能需要修改 configure 或 cmake 脚本。

Emscripten 链接器输出文件

除非使用某些特定的标志(例如 -c-S-r-shared)运行,否则 emcc 将运行链接阶段,该阶段可能产生不止一个文件。生成的 文件集根据传递给 emcc 的最终标志和指定输出文件的名称而变化。以下是一个简表,说明了在不同条件下会生成哪些文件

  • emcc ... -o output.html 构建 output.html 文件作为输出,以及一个配套的 output.js 启动器文件和一个 output.wasm WebAssembly 文件。

  • emcc ... -o output.js 不生成 HTML 启动器文件(假设您会在计划在浏览器中运行时自己提供它),并生成两个文件,output.jsoutput.wasm。(可以在例如 node.js shell 中运行)

  • emcc ... -o output.wasm 不生成 JavaScript 或 HTML 启动器文件,并生成一个独立模式构建的 Wasm 文件,就像使用了 -sSTANDALONE_WASM 设置一样。生成的 文件期望使用 WASI ABI 运行 - 特别地,一旦您初始化了模块,您必须在对它执行任何其他操作之前手动调用 _start 导出(在 --no-entry 的情况下)或 _initialize 导出。

  • emcc ... -o output.{html,js} -sWASM=0 使编译器以 JavaScript 为目标,因此不会生成 .wasm 文件。

  • emcc ... -o output.{html,js} --emit-symbol-map 如果 WebAssembly 是目标(未指定 -sWASM=0),或者如果 JavaScript 是目标并且指定了 -Os-Oz-O2 或更高,但调试级别设置为 -g1 或更低(即如果发生了符号缩减),则会生成一个文件 output.{html,js}.symbols

  • emcc ... -o output.{html,js} -gsource-map 生成一个源映射文件 output.wasm.map。如果以 -sWASM=0 为目标使用 JavaScript,则文件名是 output.{html,js}.map

  • emcc ... -o output.{html,js} --preload-file xxx 指令会生成一个预加载的 MEMFS 文件系统文件 output.data

  • emcc ... -o output.{html,js} -sWASM={0,1} -sSINGLE_FILE 将 JavaScript 和 WebAssembly 代码合并到单个输出文件 output.{html,js}(以 base64 格式)中,以便只生成一个文件用于部署。(如果与 --preload-file 配合使用,预加载的 .data 文件仍作为单独的文件存在)

此列表并不详尽,但说明了最常用的组合。

注意

无论输出文件的名称是什么,emcc 将始终执行链接并生成最终的可执行文件,除非特定的标志(例如 -c)指示它执行其他操作。这与之前行为不同,在之前行为中,emcc 默认情况下会组合对象文件(基本上假设 -r),除非给定了特定的可执行文件扩展名(例如 .js.html)。

使用优化构建项目

Emscripten 在两个级别上执行 编译器优化:每个源文件在被编译成对象文件时由 LLVM 优化,然后在将对象文件转换为最终的 JavaScript/WebAssembly 时,会应用特定于 JavaScript/WebAssembly 的优化。

为了正确优化代码,通常最好在将源代码编译为目标代码以及将目标代码编译为 JavaScript(或 HTML)时使用相同优化标志和其他编译器选项

请参考以下示例

# Sub-optimal - JavaScript/WebAssembly optimizations are omitted
emcc -O2 a.cpp -c -o a.o
emcc -O2 b.cpp -c -o b.o
emcc a.o b.o -o project.js

# Sub-optimal - LLVM optimizations omitted
emcc a.cpp -c -o a.o
emcc b.cpp -c -o b.o
emcc -O2 a.o b.o -o project.js

# Usually the right thing: The same options are provided at compile and link.
emcc -O2 a.cpp -c -o a.o
emcc -O2 b.cpp -c -o b.o
emcc -O2 a.o b.o -o project.js

但是,有时您可能希望对某些文件进行略微不同的优化

# Optimize the first file for size, and the rest using `-O2`.
emcc -Oz a.cpp -c -o a.o
emcc -O2 b.cpp -c -o b.o
emcc -O2 a.o b.o -o project.js

注意

不幸的是,每个构建系统都定义了自己的设置编译器和优化方法的机制。您需要找出为您的系统设置 LLVM 优化标志的正确方法

  • 一些构建系统具有类似./configure --enable-optimize的标志。

JavaScript/WebAssembly 优化是在最后一步(有时称为“链接”,因为该步骤通常还会将许多文件链接在一起,这些文件都编译成一个 JavaScript/WebAssembly 输出)中指定的。例如,要使用-O1编译

# Compile the object file to JavaScript with -O1 optimizations.
emcc -O1 project.o -o project.js

使用调试信息构建项目

构建包含调试信息的项目要求在 LLVM 和 JavaScript 编译阶段都指定调试标志。

为了使Clang 和 LLVM 在目标文件中发出调试信息,您需要使用-g编译源代码(与clanggcc 通常一样)。

注意

每个构建系统都定义了自己的设置调试标志的机制。为了使 Clang 发出 LLVM 调试信息,您需要找出适合您的系统的正确方法

  • 一些构建系统具有类似./configure --enable-debug的标志。在基于CMake 的构建系统中,将CMAKE_BUILD_TYPE 设置为"Debug"

为了使emcc 在生成最终的 JavaScript 和 WebAssembly 时包含目标文件中的调试信息,您的最终emcc 命令必须指定-g-gN 中的某个调试级别选项

# Compile the Wasm object file to JavaScript+WebAssembly, with debug info
# -g or -gN can be used to set the debug level (N)
emcc -g project.o -o project.js

有关更一般的信息,请参阅主题调试

使用库

提供对多个标准库的内置支持:libclibc++SDL。当您编译使用它们的代码时,这些库将自动链接(您甚至不需要添加-lSDL,但请参见下文了解有关 SDL 的更多特定详细信息)。

如果您的项目使用其他库,例如zlibglib,您需要构建并链接它们。通常的方法是构建库(生成目标文件或它们的.a 存档),然后将它们与您的主程序链接以生成 JavaScript+WebAssembly。

例如,考虑一个项目“project”使用库“libstuff”的情况

# Compile libstuff to libstuff.a
emconfigure ./configure
emmake make

# Compile project to project.o
emconfigure ./configure
emmake make

# Link the library and code together.
emcc project.o libstuff.a -o final.html

Emscripten 端口

Emscripten 端口是移植到 Emscripten 的一系列有用库。它们位于GitHub 上,并在emcc 中具有集成支持。当您请求使用某个端口时,emcc 将从远程服务器获取它,在本地进行设置和构建,然后将其与您的项目链接,将必要的包含添加到您的构建命令中,等等。例如,SDL2 位于端口中,您可以使用--use-port=sdl2 请求使用它。例如,

emcc test/browser/test_sdl2_glshader.c --use-port=sdl2 -sLEGACY_GL_EMULATION -o sdl2.html

您应该会看到有关 SDL2 的一些使用通知,以及如果它以前没有构建,它将被构建。然后,您可以在浏览器中查看sdl2.html

要查看所有可用端口的列表,请运行emcc --show-ports

注意

SDL_image 也已添加到端口,可以使用--use-port=sdl2_image 使用它。为了使sdl2_image 有用,您通常需要使用例如--use-port=sdl2_image:formats=bmp,png,xpm,jpg 指定您计划使用的图像格式。这还将确保当您指定这些格式时,IMG_Init 将正常工作。或者,您可以使用emcc --use-preload-plugins--preload-file 预加载图像,以便浏览器编解码器对它们进行解码(请参阅预加载文件)。sdl2_image 端口中的一个代码路径将通过emscripten_get_preloaded_image_data() 加载,但随后您对使用这些图像格式的IMG_Init 的调用将失败(因为虽然图像将通过预加载工作,但 IMG_Init 报告不支持这些格式,因为它没有编译支持 - 换句话说,IMG_Init 不会报告对仅通过预加载工作的格式的支持)。

注意

SDL_net 也已添加到端口,可以使用--use-port=sdl2_net 使用它。

注意

Emscripten 还支持旧的 SDL1,它是内置的。如果您没有像上面的命令那样指定 SDL2,那么 SDL1 将被链接,并且将使用 SDL1 包含路径。SDL1 支持sdl-config,它位于system/bin 中。使用本机sdl-config 可能会导致编译或缺少符号错误。您需要修改构建系统,以便在emscripten/systememscripten/system/bin 中查找文件,才能使用 Emscripten sdl-config

注意

如果您愿意,您还可以以手动方式从端口构建库,但您还需要应用端口执行的 Python 逻辑。该代码(位于tools/ports/ 下)可能做一些事情,例如确保必要的 JS 函数包含在构建中,添加导出等等。一般来说,最好使用端口版本,因为它是经过测试且已知有效的版本。

注意

从 emscripten 3.1.54 开始,--use-port 是在您的项目中使用端口的首选语法。旧语法(例如-sUSE_SDL2-sUSE_SDL_IMAGE=2)仍然可用。

贡献端口

贡献端口由更广泛的社区贡献,并以“尽力而为”的方式提供支持。由于它们不是作为 emscripten CI 的一部分运行的,因此不能始终保证它们可以构建或正常运行。有关更多信息,请参阅贡献端口

添加更多端口

添加新端口的最简单方法是将其放在contrib 目录下。基本上,步骤如下:

  • 确保端口是开源的并且具有合适的许可证。

  • 阅读tools/ports/contrib 下的README.md 文件,其中包含更多信息。

外部端口

Emscripten 还支持外部端口(不属于分发的端口)。要使用这样的端口,您只需提供其路径:--use-port=/path/to/my_port.py

注意

请注意,如果您正在处理端口代码,则 emscripten 使用的端口 API 并非 100% 稳定,并且可能会在不同版本之间发生变化。

构建系统问题

构建系统自执行

一些大型项目会生成可执行文件并运行它们,以生成构建过程后期部分的输入(例如,可以构建解析器,然后在语法上运行它,然后生成实现该语法的 C/C++ 代码)。当使用 Emscripten 时,这种构建过程会引起问题,因为您无法直接运行正在生成的代码。

最简单的解决方案通常是构建项目两次:一次以原生方式构建,一次构建为 JavaScript。当 JavaScript 构建过程由于缺少生成的执行文件而失败时,您可以从原生构建中复制该执行文件,然后继续正常构建。例如,这种方法已成功用于编译 Python(它需要在构建期间运行其pgen 可执行文件)。

在某些情况下,修改构建脚本以便它们以原生方式构建生成的执行文件是有意义的。例如,可以通过在构建脚本中指定两个编译器emccgcc,以及仅对生成的执行文件使用gcc 来完成此操作。但是,这可能比前面的解决方案更复杂,因为您需要修改项目构建脚本,并且您可能必须解决代码被编译并用于最终结果和生成的执行文件这两种情况的问题。

伪动态链接

Emscripten 的目标是生成尽可能快且尽可能小的代码。为此,它专注于将整个项目编译成单个 Wasm 文件,尽可能避免动态链接。

默认情况下,当使用-shared 标志构建共享库时,Emscripten 将生成一个.so 库,该库实际上只是一个普通的.o 目标文件(在幕后,它使用ld -r 将目标文件组合成一个更大的目标文件)。当这些伪“共享库”链接到您的应用程序时,它们实际上是作为静态库链接的。当构建这些共享库时,Emcc 会忽略命令行上的其他共享库。这样做是为了确保在中间构建阶段不会多次链接同一个动态库,这会导致重复符号错误。

有关如何构建真正的动态库(可以在加载时或在运行时(通过 dlopen)链接在一起)的信息,请参阅实验性支持

Configure 可能会运行似乎失败的检查

使用configurecmake或其他可移植配置方法的项目可能会在配置阶段运行检查,以验证工具链和路径是否设置正确。Emcc会尝试尽可能地通过检查,但您可能需要禁用因“假阴性”而失败的测试(例如,在最终执行环境中通过但不在configure期间的 shell 中通过的测试)。

提示

确保如果禁用检查,则测试的功能确实有效。这可能涉及使用特定于构建系统的特定方法手动将命令添加到 make 文件中。

注意

通常,configure不适合像Emscripten这样的交叉编译器。configure旨在为本地设置进行本地构建,并努力寻找本地构建系统和本地系统头文件。使用交叉编译器时,您将针对不同的系统,并忽略这些头文件等。

存档(.a)文件

Emscripten支持.a存档文件,它们是目标文件的捆绑包。这是一种用于库的简单格式,具有特殊的语义 - 例如,链接顺序对.a文件很重要,但对普通目标文件不重要。在大多数情况下,这些特殊语义在Emscripten中的工作方式与其他地方相同。

手动使用emcc

Emscripten教程展示了如何使用emcc将单个文件编译成JavaScript。Emcc也可以用于您期望gcc完成的所有其他方式。

# Generate a.out.js from C++. Can also take .ll (LLVM assembly) or .bc (LLVM bitcode) as input
emcc src.cpp

# Generate an object file called src.o.
emcc src.cpp -c

# Generate result.js containing JavaScript.
emcc src.cpp -o result.js

# Generate an object file called result.o
emcc src.cpp -c -o result.o

# Generate a.out.js from two C++ sources.
emcc src1.cpp src2.cpp

# Generate object files src1.o and src2.o
emcc src1.cpp src2.cpp -c

# Combine two object files into a.out.js
emcc src1.o src2.o

# Combine two object files into another object file (not normally needed)
emcc src1.o src2.o -r -o combined.o

# Combine two object files into library file
emar rcs libfoo.a src1.o src2.o

除了与gcc共享的功能之外,emcc还支持优化代码、控制发射哪些调试信息、生成HTML和其他输出格式等的选项。这些选项在emcc工具参考(命令行上的emcc --help)中进行了文档说明。

在预处理器中检测Emscripten

Emscripten提供以下预处理器宏,可用于标识编译器版本和平台

  • 使用Emscripten编译程序时,始终定义预处理器定义__EMSCRIPTEN__

  • 预处理器变量__EMSCRIPTEN_major____EMSCRIPTEN_minor____EMSCRIPTEN_tiny__emscripten/version.h中定义,并以整数形式指定当前使用的Emscripten编译器版本。

  • Emscripten的行为类似于Unix的变体,因此预处理器定义unix__unix__unix__在使用Emscripten编译代码时始终存在。

  • Emscripten使用Clang/LLVM作为其底层代码生成编译器,因此定义了预处理器定义__llvm____clang__,并且预处理器定义__clang_major____clang_minor____clang_patchlevel__表示使用的Clang版本。

  • Clang/LLVM与GCC兼容,因此预处理器定义__GNUC____GNUC_MINOR____GNUC_PATCHLEVEL__也被定义为表示Clang/LLVM提供的GCC兼容性级别。

  • 预处理器字符串__VERSION__表示GCC兼容版本,它被扩展为还显示Emscripten版本信息。

  • 同样,__clang_version__存在,并指示Emscripten和LLVM版本信息。

  • Emscripten是一个32位平台,因此size_t是一个32位无符号整数,__POINTER_WIDTH__=32__SIZEOF_LONG__=4,并且__LONG_MAX__等于2147483647L

  • 当使用命令行编译器标志之一-msse-msse2-msse3-mssse3-msse4.1针对SSEx SIMD API时,预处理器标志之一__SSE____SSE2____SSE3____SSSE3____SSE4_1__将存在以指示对这些指令集的支持。

  • 如果使用编译器和链接器标志-pthread针对pthreads多线程支持,则预处理器定义__EMSCRIPTEN_PTHREADS__将存在。

使用编译器包装器

有时,为了执行诸如ccachedistccgomacc之类的操作,使用编译器包装器会很有用。对于ccache,简单地包装整个编译器应该可以使用正常方法,例如ccache emcc。对于分布式构建,在本地运行emscripten驱动程序并仅分发底层的clang命令可能会有利。如果需要,配置文件中的COMPILER_WRAPPER设置可用于在对clang的内部调用添加包装器。与其他配置设置一样,这也可以通过环境变量进行设置。例如

EM_COMPILER_WRAPPER=gomacc emcc -c hello.c

pkg-config

emconfigureemmake为交叉编译配置pkg-config,并设置环境变量PKG_CONFIG_LIBDIRPKG_CONFIG_PATH。要提供自定义pkg-config路径,请设置环境变量EM_PKG_CONFIG_PATH

示例/测试代码

Emscripten测试套件(test/runner.py)包含许多很好的示例 - 使用上面描述的正常构建系统构建的大型C/C++项目:freetypeopenjpegzlibbulletpoppler.

也值得查看ammo.js项目中的构建脚本。

故障排除

  • 确保使用emar(它调用llvm-ar),因为系统ar可能不支持我们的目标文件。 emmakeemconfigure会正确设置AR环境变量,但构建系统可能会错误地硬编码ar

  • 类似地,使用系统ranlib而不是emranlib(它调用llvm-ranlib)可能会导致问题,例如不支持我们的目标文件并删除索引,从而导致archive has no index; run ranlib to add one来自wasm-ld。同样,使用emmake/emconfigure应该通过设置env var RANLIB来避免这种情况,但构建系统可能会将其硬编码,或者要求您传递选项

  • 编译错误multiply defined symbol表明该项目已多次链接特定静态库。该项目需要更改,以便问题库仅链接一次。

    注意

    您可以使用llvm-nm查看每个目标文件中定义了哪些符号。

    一种解决方案是使用动态链接。这确保库在最终构建阶段仅链接一次。

  • 在生成独立的Wasm时,请确保在尝试使用模块之前调用_start或(对于--no-entry_initialize导出。