本文目录 14 个章节
Emscripten-C++编译流程
Emscripten-C++ 编译流水线
目标:将 C/C++ 源文件构建为可在浏览器或 Node.js 中运行的 WebAssembly (.wasm)。 传统 C++ 流程的 预处理、AST 生成、IR 优化 与此处完全一致,以下仅作一行提及;其余 与原生 ELF/PE 流程不同的环节 逐步展开。
0 · 驱动入口
emcc hello.cpp -O2 -sSTANDALONE_WASM=1 -o hello.wasm
emcc 是官方 Python 封装脚本,内部调用 Clang → wasm-ld → Binaryen 这条 LLVM-系工具链。流程如下。
1 · 预处理(与传统一致,简述)
emcc -E hello.cpp -o hello.i
- 展开宏与头文件,得到
hello.i(纯文本)。 - 与 GCC/Clang 原生流程完全相同,故不再赘述。
2 · 前端解析 → LLVM IR(与传统一致,简述)
emcc -emit-llvm -S hello.cpp -o hello.ll
- Clang 生成 LLVM IR(文本
.ll或二进制.bc)。 - 若启用
-flto,Bitcode 在稍后链接阶段参与全局优化。
; ModuleID = 'hello.cpp'
source_filename = "hello.cpp"
target datalayout = "..."
target triple = "x86_64-unknown-linux-gnu"
@_ZSt4cout = external dso_local global %class.std::ostream, align 8
; 定义 square
define dso_local i32 @_Z6squarei(i32 %x) #0 {
entry:
%mul = mul nsw i32 %x, %x
ret i32 %mul
}
; 定义 main
define dso_local i32 @_Z4main() #0 {
entry:
; 调用 square(3)
%call = call i32 @_Z6squarei(i32 3)
; 打印
%0 = call %class.std::ostream* @_ZStlsISt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_PKc(
%class.std::ostream* @_ZSt4cout, i8* getelementptr inbounds ([7 x i8], [7 x i8]* @.str, i32 0, i32 0))
; ...
ret i32 0
}
3 · LLVM IR → Wasm Relocatable Object(差异重点)
emcc -c hello.cpp -o hello.o # 默认 -sWASM_OBJECT_FILES=1
目标后端切换
LLVM 选用
wasm32-unknown-emscripten后端,将 SSA-IR 翻译为 WebAssembly 指令流,而非 x86/AArch64 机器码。 封装格式产物
hello.o是 Wasm Relocatable Object(非 ELF):- 含
.text(函数)、.data(静态数据) 等节区; - 保留重定位表,供后续链接器合并。
- 内部仍是 LLVM IR bitcode,但用 Wasm 格式封装。
- 含
*调试查看
llvm-objdump -d hello.o # 反汇编 Wasm 指令
LLVM Wasm 后端自 3.37+ 版本起已默认稳定,可直接生成可链接对象文件。(github.com)
4 · 归档静态库(与传统基本一致,简述)
emar rcs libhello.a hello.o
emar行为等同 GNUar,成员可为.o或.bc。若希望在链接阶段做 ThinLTO,可直接归档
.bc文件而非.o。Bitcode Archive(Emscripten 默认):当 bitcode 构建开启时,
libhello.a的成员是 LLVM IR bitcode 模块(.bc或.o内含 bitcode)。
- 用
ar t libhello.a可以列出成员文件;file <member>能区分是 ELF 还是 LLVM bitcode。
llvm-objdump --headers xxx.a
Idx Name Size VMA Type
0 TYPE 00000073 00000000
1 IMPORT 000009d7 00000000
2 FUNCTION 00000627 00000000
3 ELEM 000000c2 00000000
4 DATACOUNT 00000002 00000000
5 CODE 000430ba 00000000 TEXT
6 DATA 00001221 00000000 DATA
7 .debug_loc 000000ae 00000000
8 .debug_abbrev 00000b32 00000000
9 .debug_info 00034bde 00000000
10 .debug_ranges 000031d8 00000000
11 .debug_str 00058a86 00000000
12 .debug_line 00016596 00000000
13 linking 000471de 00000000
14 reloc.CODE 0000a78f 00000000
15 reloc.DATA 00000543 00000000
16 reloc..debug_loc 0000001d 00000000
17 reloc..debug_info 0001ee19 00000000
18 reloc..debug_ranges 00004e9f 00000000
19 reloc..debug_line 000029d2 00000000
20 producers 00000086 00000000
21 target_features 0000000d 00000000
5 · 链接:wasm-ld + Binaryen(差异重点)
emcc main.o libhello.a -O2 -sSTANDALONE_WASM=1 -o hello.wasm
- wasm-ld
- 解析符号、执行 LTO(若输入为
.bc)。 - 完成地址布局与重定位,输出初步
a.out.wasm。(stackoverflow.com)
- 解析符号、执行 LTO(若输入为
- wasm-opt(Binaryen)
- 根据
-O0/1/2/3/s/z级别进行 Wasm-SSA 优化、栈化、死码删除、SIMD 展开等。 - 体积与性能的最后一跳压缩。(github.com, web.dev)
- 根据
生成结果:
hello.wasm—— 纯二进制 WebAssembly。不包含它那一套运行时 glue(文件系统、SDL、堆内存管理等 JS 接口),只输出一个自洽的.wasm模块。- 适合在你自己的加载器(或 Node.js、Unity、嵌入式)里直接用
WebAssembly.instantiate。
6 · Glue 代码与打包(差异重点)
# Emscripten-C++编译流程
emcc hello_wasm.o \
-O2 \
--emit-symbol-map \
-o hello.html
- 输出名决定打包形式
-o 目标 |
生成文件 | 说明 |
|---|---|---|
hello.wasm |
仅 .wasm |
纯 Wasm,适合自行加载 |
hello.js |
.wasm + .js |
JS 加载器(含 Emscripten runtime) |
hello.html |
.wasm + .js + .html |
可即点即开的测试页 |
.js文件里包含了 Emscripten runtime(堆管理、文件 I/O、C 库接口).html只是给你一个“点它就能跑”的 demo 页面
- 运行示例(纯 Wasm)
node --experimental-wasi-unstable-preview1 -e "const fs=require('fs');const wabt=require('wabt')"
## 或在浏览器中通过 `<script type="module">...</script>` 手动 fetch & instantiate
7 · 整体回顾
相同步骤(仅简述) 预处理 → AST/IR 生成 → IR 优化 → (可选)静态库归档。
关键差异(已详细)
- LLVM wasm 后端输出 Wasm Relocatable .o;
- 链接器换成 wasm-ld,随后由 Binaryen wasm-opt 做指令级压缩与改写;
- 产物不再是 ELF,可选择纯
.wasm或附带 JS/HTML Glue。
按照以上流程,即可从标准 C/C++ 代码生成满足 Web 平台运行约束、并经过专用优化的 WebAssembly 文件。
WebAssembly程序的一些特点
内存模型简述
- WebAssembly 只有一块 线性内存,按页(64 KB)扩展。
- 分段:静态区 + 栈 + 堆;JS 侧通过
HEAP8/HEAP32/...视图直接访问。 - 想让堆自动增长:
-sALLOW_MEMORY_GROWTH=1。
虚拟文件系统
- 内置 MEMFS(纯内存)、IDBFS(落盘到 IndexedDB)、WORKERFS 等。
- 静态打包:
--preload-file/--embed-file。 - 运行期挂载:
FS.mount(FS.filesystems.IDBFS, {}, '/data')。
多线程
- 通过 PThreads ↔ Web Workers 转译。
- 关键开关:
-sUSE_PTHREADS=1(同时要在目标环境启用 SharedArrayBuffer)。 - Unity WebGL 构建需在 Player Settings 勾选 “Threads Support”。
压缩与体积优化
- Emscripten 本身不压缩输出。
- Unity WebGL 会额外生成
.wasm.br / .wasm.gz并在启动脚本中自解压。 - 部分小程序平台(如微信小游戏)不支持自解压启动,需要关闭 Unity 的压缩或自行在服务器侧压缩后手动解压。