Emscripten 项目构建指南:使用 emconfigure / emcmake / emmake 将现有构建系统移植到 WebAssembly
2026/9/20 18:27:11 网站建设 项目流程
  • 编译器
  • WebAssembly
  • 开发工具
  • 构建工具

【免费下载链接】emscripten

Emscripten: An LLVM-to-WebAssembly Compiler

项目地址:https://gitcode.com/gh_mirrors/em/emscripten
点击查看免费下载

导读

本文是 Emscripten 官方文档 "Building Projects" 的深度解读。Emscripten 通过emconfigureemcmakeemmake三个辅助脚本,让现有的 configure/autotools、CMake 乃至其他构建系统以emcc作为gcc的直接替代品完成交叉编译,绝大多数情况下无需改动项目自身的构建脚本。读完本文,你将掌握把任意 C/C++ 项目构建为 JavaScript + WebAssembly 的完整流程、emcc链接产物的文件清单、优化与调试参数在两阶段编译中的正确用法、Emscripten Ports 库的接入方式,以及常见构建问题的排查手段。


一、构建系统接入:三个辅助脚本的分工

Emscripten 提供三个环境变量注入工具,它们的核心逻辑都位于仓库根目录的脚本中:

脚本适用构建系统核心行为
emconfigureconfigure / autotoolsemcc/em++等覆盖CC/CXX等环境变量,并设置EMMAKEN_JUST_CONFIGURE=1让 configure 阶段生成原生可执行文件以通过编译检查
emcmakeCMake自动追加-DCMAKE_TOOLCHAIN_FILE=<仓库>/cmake/Modules/Platform/Emscripten.cmake-DCMAKE_CROSSCOMPILING_EMULATOR=<node>
emmake无独立配置阶段的 make 项目make注入与emconfigure相同的构建环境变量

三者最终都通过 tools/building.py 中的get_building_env()构造环境。该函数的具体行为(可在 emconfigure.py、emmake.py、emcmake.py 中交叉验证):

env['CC'] = EMCC # C 编译器 env['CXX'] = EMXX # C++ 编译器 env['AR'] = EMAR # llvm-ar env['LD'] = EMCC env['NM'] = LLVM_NM env['LDSHARED'] = f'{EMCC} -shared' env['RANLIB'] = EMRANLIB # llvm-ranlib env['HOST_CC'] = CLANG_CC # 宿主编译用 clang env['HOST_CXX'] = CLANG_CXX env['PKG_CONFIG_LIBDIR'] = <sysroot>/local/lib/pkgconfig:<sysroot>/lib/pkgconfig env['PKG_CONFIG_PATH'] = $EM_PKG_CONFIG_PATH env['PATH'] = <sysroot>/bin:... env['ACLOCAL_PATH'] = <sysroot>/share/aclocal env['CROSS_COMPILE'] = <仓库>/em

注意emconfigureemmake的本质区别:emconfigure.py 会额外设置EMMAKEN_JUST_CONFIGURE=1,此时编译产物是原生代码(用于让 configure 的自检程序真正运行起来);而emmake阶段则用 Emscripten 生成 WebAssembly 目标文件。

1.1 Configure / Autoconf 项目

假设你平时这样构建:

./configure make

改用 Emscripten 后:

# 用 emconfigure 包裹 configure,让它检测到 emcc 工具链 emconfigure ./configure # 运行 make,产出 Wasm 目标文件 make # 将 make 产出的目标文件链接为 JavaScript + WebAssembly # project.o 需替换为项目实际输出;若输出后缀特殊(如 project.so、project.so.1) # 或可执行文件没有后缀,可能需要重命名 # 若输出是库,还需在链接命令中附上你的 main.c # [-Ox] 代表编译优化级别(见后文优化章节) emcc [-Ox] project.o -o project.js

如果对 configure/make 机制不熟悉,可以把它理解为「configure 探测工具链并生成 Makefile,make 按 Makefile 编译」,Emscripten 只是让探测和编译都指向了emcc

1.2 CMake 项目

平时构建:

cmake -B build cmake --build build

改用 Emscripten:

# emcmake 自动把 Emscripten 工具链文件传给 CMake emcmake cmake -B build # 构建阶段照常使用 cmake --build(或 make) cmake --build build

emcmake的关键行为来自 emcmake.py:

  • 若命令行与环境变量中都没有指定-DCMAKE_TOOLCHAIN_FILE,则自动追加-DCMAKE_TOOLCHAIN_FILE=<仓库>/cmake/Modules/Platform/Emscripten.cmake
  • 自动追加-DCMAKE_CROSSCOMPILING_EMULATOR=<node>,使 CMake 的 try-run 类测试能够通过 node 执行编译出的 Wasm(相关背景见仓库内注释,对应 issue 15522);
  • 在 Windows 上若未指定-G,会优先选择 MinGW Makefiles 或 Ninja 生成器,避免 CMake 拉入原生 Visual Studio 工具链。

工具链文件本身位于 cmake/Modules/Platform/Emscripten.cmake,其中定义了CMAKE_C_COMPILER=emccCMAKE_CXX_COMPILER=em++以及 Wasm 平台相关的编译器与链接器参数。

1.3 其他构建系统

不使用 configure/CMake 的项目可以省略配置阶段,直接:

emmake make

但要注意:如果 Makefile 中硬编码了CC/CXX等编译器变量,可能需要手动修改 Makefile。同时请记住 emmake.py 源码注释中的提示:

如果在配置阶段使用了emconfigureemcmake,构建阶段通常不需要emmake——编译器设置已固化在生成的构建文件中。emmake主要服务于没有独立配置阶段的项目。

make会生成 Wasm 目标文件,也可能把目标文件链接为库或 Wasm 可执行文件。除非构建系统已被改造为直接输出 JavaScript,否则你仍需要按 1.1 中的方式额外执行一次emcc命令,产出最终可运行的 JavaScript + WebAssembly。

1.4 make 产物的文件后缀

make输出的文件后缀与 gcc 体系一致:

后缀含义
.a静态库归档
.so共享库
.o目标文件

无论后缀如何,这些文件都包含emcc可消费的内容:通常是 Wasm 目标文件,若开启了 LTO 则包含 LLVM bitcode。它们都能被emcc编译进最终的 JavaScript + WebAssembly。

1.5 产物有效性检查

某些构建系统可能没有正确产出 Wasm 目标文件,链接时会出现is not a valid input file警告。排查手段:

# 用 file 命令查看文件真实类型 file project.o # 手动检查魔数:Wasm 目标文件以 \0asm 开头,LLVM bitcode 以 BC 开头 # 查看 make 实际执行的命令,确认用的是 emcc 而非系统编译器 emmake make VERBOSE=1

如果VERBOSE=1显示仍在调用原生编译器,就需要修改 configure 或 cmake 脚本,让编译器指向emcc


二、Emscripten 链接器输出文件清单

除非带有-c-S-r-shared等特定标志,否则emcc都会进入链接阶段,且可能产出不止一个文件。产出集合取决于最终传给emcc的标志与输出文件名。以下是常用组合速查表:

命令产物说明
emcc ... -o output.htmloutput.html+output.js(启动器)+output.wasm标准浏览器部署形态
emcc ... -o output.jsoutput.js+output.wasm不生成 HTML 启动器,可在 node.js 中直接运行;若要在浏览器运行需自备 HTML
emcc ... -o output.wasmoutput.wasm-sSTANDALONE_WASM的独立模式构建,遵循 WASI ABI;初始化后必须手动调用_start导出(--no-entry时调用_initialize),之后才能做其他操作
emcc ... -o output.{html,js} -sWASM=0仅 JS 产物,无.wasm文件目标改为 JavaScript(asm.js 风格)
emcc ... -o output.{html,js} --emit-symbol-mapoutput.{html,js}.symbols仅当面向 WebAssembly(未指定-sWASM=0);或面向 JS 且指定-Os/-Oz/-O2及以上、调试级别为-g1及以下(即确实发生了符号压缩)时产出
emcc ... -o output.{html,js} -gsource-mapoutput.wasm.map面向 JS(-sWASM=0)时文件名为output.{html,js}.map
emcc ... -o output.{html,js} --preload-file xxxoutput.data预加载的 MEMFS 文件系统数据文件
emcc ... -o output.{html,js} -sWASM={0,1} -sSINGLE_FILEoutput.{html,js}以 base64 形式把 JS 与 Wasm 合并进单个文件;若搭配--preload-file.data文件仍单独存在

此清单未穷尽所有情况,但覆盖了最常见的组合。

重要行为变化:无论输出文件名是什么,emcc总是执行链接并产出最终可执行文件(除非-c等标志改变行为)。这与旧版行为不同——旧版emcc在未指定可执行扩展名(如.js/.html)时,默认只是合并目标文件(相当于-r)。


三、带优化地构建项目

Emscripten 的优化发生在两个层级(详见 emcc.txt 的优化选项说明):

  1. LLVM 层:每个源文件被编译为目标文件时,由 LLVM 进行优化;
  2. JavaScript/WebAssembly 层:目标文件转换为最终 JS/Wasm 时,应用 JS/Wasm 特有的优化。

要获得最佳优化效果,通常应该在「源码 → 目标文件」和「目标文件 → JavaScript/HTML」两步使用相同的优化标志与其他编译选项。

# 次优:缺少 JS/Wasm 层优化(链接时无 -O2) emcc -O2 a.cpp -c -o a.o emcc -O2 b.cpp -c -o b.o emcc a.o b.o -o project.js # 次优:缺少 LLVM 层优化(编译时无 -O2) emcc a.cpp -c -o a.o emcc b.cpp -c -o b.o emcc -O2 a.o b.o -o project.js # 推荐做法:编译与链接使用相同优化选项 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

有时需要针对特定文件使用不同优化:

# 第一个文件按体积优化(-Oz),其余用 -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的开关。

JS/Wasm 层优化在最后一步(常被称为 "link",因为该步骤通常会把多个文件链接进一个 JS/Wasm 输出)指定。例如:

# 以 -O1 优化级别把目标文件编译为 JavaScript emcc -O1 project.o -o project.js

四、带调试信息地构建项目

要让项目包含调试信息,需要在LLVM 编译阶段JavaScript 编译阶段都指定调试标志。

  1. 让 Clang/LLVM 在目标文件中生成调试信息:编译源码时加-g(与 clang/gcc 用法完全一致)。各构建系统设置方式不同:有的提供./configure --enable-debug;CMake 项目则设置CMAKE_BUILD_TYPE"Debug"

  2. 让 emcc 在最终产物中包含调试信息:最后的emcc链接命令必须指定-g或某个-gN调试级别选项:

# 把 Wasm 目标文件编译为带调试信息的 JavaScript+WebAssembly # -g 或 -gN 用于设置调试级别(N 为级别数字) emcc -g project.o -o project.js

更通用的调试主题参见文档中的 Debugging 专题。


五、使用库

Emscripten 对若干标准库提供内建支持libclibc++SDL。使用这些库的代码会被自动链接,甚至无需显式添加-lSDL

如果项目使用其他库(例如 zlib 或 glib),则需要自行构建并链接:常规做法是把库构建为目标文件或.a归档,再与主程序一起链接产出 JavaScript+WebAssembly。例如项目 "project" 依赖库 "libstuff":

# 构建 libstuff 为 libstuff.a emconfigure ./configure emmake make # 构建 project 为 project.o emconfigure ./configure emmake make # 将库与代码链接在一起 emcc project.o libstuff.a -o final.html

六、Emscripten Ports 库

Emscripten Ports是一组移植到 Emscripten 的实用库集合,位于仓库的 tools/ports 目录(包含sdl2.pysdl2_image.pysdl2_mixer.pysdl2_ttf.pysdl2_net.pysdl2_gfx.pyzlib.pylibpng.pyfreetype.pybullet.pybzip2.pyharfbuzz.pyicu.pysqlite3.pyvorbis.pyogg.py等端口脚本),并与emcc深度集成。当你在编译命令中请求某个 port 时,emcc会下载、构建并安装它到 emscripten sysroot。

例如,在项目中使用 SDL2 port,只需在编译和链接标志中加入--use-port=sdl2

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

首次使用某个 port 时,编译过程中会看到下载与安装的通知。

常用操作:

# 查看所有可用 ports emcc --show-ports # 用独立工具 embuilder 预先构建 port(例如构建 SDL2) ./embuilder build sdl2 # 查看 embuilder 全部可用目标 embuilder --help

关键特性:port 一旦构建并安装到 sysroot,就会对所有后续emcc命令可见。例如运行过带--use-port=sdl2emcc或执行过./embuilder build sdl2之后,后续emcc命令都能找到 SDL2 的头文件与库。

语法说明:自 Emscripten 3.1.54 起,--use-port是使用 port 的推荐语法;旧语法(如-sUSE_SDL2-sUSE_SDL_IMAGE=2)仍然可用。

此外,Emscripten 还内建支持较老的 SDL 1.3,通过-sUSE_SDL=1启用,并使用 sysroot 中的sdl-config<sysroot>/bin/sdl-config)。注意:使用宿主机的sdl-config可能导致编译错误,需要修改构建系统,使其在 emscripten sysroot 中查找sdl-config

6.1 Port 特定说明:sdl2_image

sdl2_imageport 通常需要指定支持的图片格式列表:

--use-port=sdl2_image:formats=bmp,png,xpm,jpg

这能保证IMG_Init在指定这些格式时正常工作。另一种方式是使用emcc --use-preload-plugins配合--preload-file预加载图片,由浏览器编解码器解码。此时sdl2_imageport 中有一条代码路径会通过emscripten_get_preloaded_image_data加载图片,但你对这些格式调用IMG_Init会失败——因为图片虽能通过预加载工作,IMG_Init却报告不支持这些格式(它并没有把这些格式编译进去)。换言之,IMG_Init不会报告只通过预加载才能工作的格式。

6.2 Contrib ports(社区贡献端口)

Contrib ports 由更广泛的社区贡献,按"尽力而为"原则维护。由于它们不参与 Emscripten 的 CI 测试,不保证总能构建或正常工作。详见文档中的 Contrib Ports 专题。

6.3 添加更多 ports

添加新 port 最简单的方式是放入contrib目录,步骤为:

  1. 确保 port 是开源的,且具有合适的许可证;
  2. 阅读 tools/ports/contrib 下的README.md,其中包含更多信息。

6.4 外部 ports

Emscripten 也支持外部 port(不属于发行版的 port),只需提供其路径即可:

--use-port=/path/to/my_port.py

注意:如果你在开发 port 代码,请注意 emscripten 使用的 port API 并非 100% 稳定,可能在版本之间变化。


七、构建系统常见问题

7.1 构建系统自执行(self-execution)

一些大型项目会构建可执行文件并在构建过程中运行它,以生成后续构建阶段的输入(例如先构建一个 parser,运行它解析文法,生成实现该文法的 C/C++ 代码)。这类构建流程在 Emscripten 下会有问题,因为你无法直接运行生成的代码。

最简单的解决方案通常是构建两次:一次原生构建,一次面向 JavaScript。当 JS 构建流程因缺少生成的可执行文件而失败时,从原生构建中拷贝该可执行文件,再继续正常构建。例如,编译 Python(构建期需要运行pgen可执行文件)时就成功采用了这种方法。

某些情况下,修改构建脚本让生成的可执行文件用原生方式构建更合理——比如在构建脚本中指定两个编译器emccgcc,仅对生成的可执行文件使用gcc。但这通常比前一种方案复杂,因为需要修改项目构建脚本,并处理"同一份代码既用于最终产物又用于生成可执行文件"的情况。

7.2 动态链接

Emscripten 的目标是生成尽可能快且小的代码,因此倾向于把整个项目编译进单个 Wasm 文件,大多数情况下建议避免动态链接。如果你的项目确实需要动态链接,参见文档中的实验性支持专题(Dynamic-Linking)。

7.3 configure 的检查可能"看似失败"

使用 configure、cmake 或其他可移植配置方法的项目,可能在配置阶段运行检查来验证工具链与路径设置是否正确。emcc会尽量让这些检查通过,但你可能需要禁用某些因"假阴性"而失败的测试(例如在最终运行环境中能通过、但在 configure 期间的 shell 中无法通过的测试)。

提示:禁用某检查前,务必确认被测功能确实可用,可能需要用构建系统特有的方式手动向 make 文件添加命令。

说明:总体而言,configure 并不适合 Emscripten 这类交叉编译器。configure 是为本地环境原生构建设计的,会努力寻找原生构建系统与本地系统头文件;而交叉编译面向的是另一套系统,应当忽略这些头文件等。

7.4 归档(.a)文件

Emscripten 支持.a归档文件(对象文件的集合)。这是库的一种简单格式,具有特殊语义——例如,.a文件的链接顺序很重要,而普通目标文件则无所谓。这些特殊语义在 Emscripten 中大体与别处一致。


八、手动使用 emcc

Tutorial 展示了用emcc把单个文件编译为 JavaScript。实际上emcc支持所有 gcc 的常规用法:

# 从 C++ 生成 a.out.js;也可接受 .ll(LLVM 汇编)或 .bc(LLVM bitcode)作为输入 emcc src.cpp # 生成名为 src.o 的目标文件 emcc src.cpp -c # 生成包含 JavaScript 的 result.js emcc src.cpp -o result.js # 生成名为 result.o 的目标文件 emcc src.cpp -c -o result.o # 从两个 C++ 源文件生成 a.out.js emcc src1.cpp src2.cpp # 生成目标文件 src1.o 和 src2.o emcc src1.cpp src2.cpp -c # 合并两个目标文件为 a.out.js emcc src1.o src2.o # 合并两个目标文件为另一个目标文件(通常不需要) emcc src1.o src2.o -r -o combined.o # 合并两个目标文件为库文件 emar rcs libfoo.a src1.o src2.o

除了与 gcc 共享的能力外,emcc还支持优化代码、控制调试信息输出、生成 HTML 与其他输出格式等选项。这些选项在 emcc 工具参考文档(命令行中执行emcc --help)中有完整记录。

注意这里使用的emar是对llvm-ar的封装(见 emar.py),emranlib则封装llvm-ranlib——它们专门支持 Emscripten 的对象文件格式。


九、在预处理器中检测 Emscripten

Emscripten 提供以下预处理器宏,用于识别编译器版本与平台:

含义
__EMSCRIPTEN__使用 Emscripten 编译时始终定义
__EMSCRIPTEN_MAJOR__/__EMSCRIPTEN_MINOR__/__EMSCRIPTEN_TINY__定义于emscripten/version.h(即 system/include/emscripten/version.h),以整数形式给出当前 Emscripten 编译器版本
unix/__unix/__unix__Emscripten 表现为 Unix 变体,这些宏始终存在
__llvm__/__clang__Emscripten 底层使用 Clang/LLVM 作为代码生成编译器
__clang_major__/__clang_minor__/__clang_patchlevel__所用 Clang 的版本
__GNUC__/__GNUC_MINOR__/__GNUC_PATCHLEVEL__Clang/LLVM 与 GCC 兼容,这些宏表示其 GCC 兼容级别
__VERSION__表示 GCC 兼容版本,展开后同时包含 Emscripten 版本信息
__clang_version__同时包含 Emscripten 与 LLVM 版本信息
size_t为 32 位无符号整数、__POINTER_WIDTH__=32__SIZEOF_LONG__=4__LONG_MAX__等于2147483647LEmscripten 是 32 位平台
__SSE__/__SSE2__/__SSE3__/__SSSE3__/__SSE4_1__使用-msse-msse2-msse3-mssse3-msse4.1之一启用相应 SIMD API 时,对应的宏会出现
__EMSCRIPTEN_PTHREADS__使用-pthread编译与链接标志启用 pthreads 多线程支持时定义

典型用法示例:

#if defined(__EMSCRIPTEN__) /* WebAssembly 平台特有代码 */ #endif

十、使用编译器包装器

有时需要使用编译器包装器(如ccachedistccgomacc)。对ccache来说,直接包装整个编译器即可:

ccache emcc

对于分布式构建,可以让 emscripten 驱动在本地运行、只分发底层的 clang 命令。此时可使用配置文件中的COMPILER_WRAPPER设置(定义于 tools/config.py,默认值为None),为内部调用 clang 的过程添加包装器。与其他配置项一样,它也可以通过环境变量设置:

EM_COMPILER_WRAPPER=gomacc emcc -c hello.c

该机制在测试套件中也有覆盖(见 test/test_other.py 附近的EM_COMPILER_WRAPPER相关测试),包括包装器脚本正常生效与失败退出的两种场景。


十一、pkg-config 支持

emconfigureemmake会为交叉编译配置pkg-config,设置环境变量PKG_CONFIG_LIBDIRPKG_CONFIG_PATH(后者默认取自EM_PKG_CONFIG_PATH环境变量,见 tools/building.py)。如需提供自定义 pkg-config 路径,设置环境变量:

export EM_PKG_CONFIG_PATH=/path/to/your/pkgconfig

十二、实例与测试代码

Emscripten 测试套件(入口为 test/runner.py)包含大量优秀实例——按上述方式用各自正常构建系统构建的大型 C/C++ 项目,包括 freetype、openjpeg、zlib、bullet 和 poppler(对应源码位于 test/third_party 下)。这些测试在 test/test_other.py 中通过test_emmake_emconfigure(验证三个辅助脚本的用法与环境变量注入)等方法被系统验证。

值得参考的还有 ammo.js 项目(一个用 Emscripten 移植 Bullet 物理引擎的著名案例)的 CMakeLists.txt 构建脚本。


十三、故障排查

  • 务必使用emar(封装llvm-ar)而非系统的ar——系统ar可能不支持我们的对象文件。emmakeemconfigure已正确设置AR环境变量,但某些构建系统可能硬编码了ar
  • 同样,使用系统的ranlib而不是emranlib(封装llvm-ranlib)可能导致问题:不支持我们的对象文件并移除索引,进而触发wasm-ldarchive has no index; run ranlib to add one错误。emmake/emconfigure通过设置RANLIB环境变量可以避免,但构建系统可能硬编码了它,或要求你显式传参。
  • 编译错误multiply defined symbol表示项目把某个静态库链接了多次,需要修改项目使问题库只被链接一次。可以用llvm-nm查看每个目标文件中定义了哪些符号。一个解决方案是使用动态链接,确保库只在最终构建阶段被链接一次。
  • 生成独立 Wasm 时,务必先调用_start(或在--no-entry时调用_initialize)导出,再使用模块。

小结

把现有 C/C++ 项目移植到 WebAssembly 的核心思路并不复杂:用emconfigure/emcmake让配置阶段认识emcc工具链,用make/cmake --build正常构建出 Wasm 目标文件,最后用emcc一次性链接为 JavaScript + WebAssembly(或 HTML)产物。在此基础上,把握优化标志在"编译/链接"两阶段的对称使用、调试信息的两阶段开启方式、Ports 与第三方库的接入,以及构建系统自执行、硬编码ar/ranlib等典型问题的处理,就能把绝大多数主流 C/C++ 项目顺利跑在浏览器与 node 环境中。

  • 编译器
  • WebAssembly
  • 开发工具
  • 构建工具

【免费下载链接】emscripten

Emscripten: An LLVM-to-WebAssembly Compiler

项目地址:https://gitcode.com/gh_mirrors/em/emscripten
点击查看免费下载
上一篇:CANN LoadData API使用指南
下一篇:7个令人惊叹的生成式AI创业公司成功案例:初创企业如何利用AI技术颠覆行业

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询