返回「计算机、信息技术与工程」

Emscripten-C++编译流程

Emscripten C++编译流程

更多
Markdown 结构化数据
本文目录 14 个章节

Emscripten-C++编译流程

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.oWasm 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 行为等同 GNU ar,成员可为 .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
  1. wasm-ld
    • 解析符号、执行 LTO(若输入为 .bc)。
    • 完成地址布局与重定位,输出初步 a.out.wasm。(stackoverflow.com)
  2. 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 · 整体回顾

  1. 相同步骤(仅简述) 预处理 → AST/IR 生成 → IR 优化 → (可选)静态库归档。

  2. 关键差异(已详细)

    • 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 的压缩或自行在服务器侧压缩后手动解压。