前置知识: C++

C++与 WebAssembly

13 min高级

C++编译为WebAssembly

前置知识

学习目标

  • 掌握「概述」的核心机制、典型用法与常见陷阱
  • 掌握「历史动机与背景」的核心机制、典型用法与常见陷阱
  • 掌握「形式化定义」的核心机制、典型用法与常见陷阱
  • 掌握「理论推导」的核心机制、典型用法与常见陷阱
  • 掌握「基础概念」的核心机制、典型用法与常见陷阱

概述

WebAssembly(缩写 Wasm)是一种可移植、紧凑、高效的二进制指令格式,由 W3C WebAssembly Working Group 标准化。它定义了一个基于栈的虚拟机(stack-based virtual machine),其指令集设计参考了 LLVM IR、asm.js 与现代 CPU 指令集。Wasm 模块可以在现代浏览器中以接近原生的速度运行,也可以在 Node.js、Deno、Bun 等 JavaScript 运行时中执行,还可以通过 WASI(WebAssembly System Interface)在服务端、边缘计算节点、嵌入式设备上运行,实现”一次编译,多处运行”。

C++ 作为系统级语言,与 Wasm 形成了天然的协同关系:

  1. 语义契合:Wasm 的线性内存(linear memory)模型直接对应 C++ 的平坦地址空间,C++ 指针运算可直接映射为 Wasm 内存偏移;
  2. 工具链成熟:LLVM 后端原生支持 Wasm 目标,Emscripten 提供完整的 C/C++ 标准库实现与 Web 桥接层;
  3. 性能优势:计算密集型任务(图像处理、物理仿真、机器学习推理、密码学)在 Wasm 中可获得 2–10 倍于 JavaScript 的性能;
  4. 复用价值:数十年来积累的 C++ 库(OpenCV、OpenSSL、SQLite、FFmpeg、Eigen)可直接编译为 Wasm,无需重写。

Wasm 的设计目标是成为一个安全、可移植、高效的”抽象汇编”层——它不是为人类编写的语言,而是为编译器生成的低级 IR。本章将从历史背景出发,深入分析 Wasm 的形式化语义、与 C++ 的语义映射、工程实践与陷阱,并通过完整的案例研究帮助读者建立端到端的 C++/Wasm 项目能力。

历史动机与背景

浏览器中的”性能瓶颈”

JavaScript 是 Web 平台的唯一原生语言,自 1995 年诞生以来一直是浏览器动态行为的载体。然而,JS 作为解释型动态语言,在以下场景下性能不足:

  • 科学计算:矩阵运算、信号处理、CFD(计算流体力学);
  • 多媒体处理:视频解码、音频滤波、图像识别;
  • 游戏与图形:3D 渲染、物理引擎、路径规划;
  • 密码学:大数运算、哈希、加解密。

为弥补性能鸿沟,业界做了多次尝试:

时间方案局限
2008NaCl(Native Client):Google 在 Chrome 中执行原生 x86 代码仅 Chrome;架构耦合;沙箱复杂
2011PNaCl(Portable NaCl):基于 LLVM IR 的可移植版本启动慢;Google 内部争议;最终废弃
2013asm.js:Mozilla 提出的 JS 子集,加 type 注解仍是文本格式,体积大;解析慢
2015WebAssembly 起草:所有主流厂商联合标准化兼容性需要时间
2017Wasm 1.0 MVP:浏览器全面支持仅整数/浮点;无 SIMD、threads
2020Wasm 2.0:SIMD、threads、reference types、bulk memory、non-trapping float接近原生性能
2023Wasm GC:垃圾回收类型;tail call;exception handling支持 Java/Kotlin 等语言
2024+WASI 0.2:组件模型(component model);异步 I/O服务端 Wasm 标准化

从 asm.js 到 WebAssembly

asm.js 的核心思想是:用 JavaScript 的子集(AOT 编译器友好)描述低级类型操作:

function add(a, b) {
    a = a | 0;       // 提示 a 为 32 位整数
    b = b | 0;
    return (a + b) | 0;
}

asm.js 通过 x | 0 强制 32 位整数语义,使引擎可以将其编译为原生指令。但其局限明显:

  • 文本格式:源码体积大(MB 级 JS 文件),下载与解析耗时;
  • 类型系统弱:只有 int32 与 float64 有 hint,复杂类型只能用 TypedArray 模拟;
  • 优化窗口长:浏览器需要解析、验证、编译,启动慢。

Wasm 直接定义二进制格式(约 asm.js 体积的 1/5),编译速度提升 10 倍以上,启动延迟从秒级降到毫秒级。它迅速成为浏览器低级代码的事实标准。

C++ 工具链的进化

工具链起源用途特点
Emscripten2010,Mozilla/Alon Zakai完整 C/C++ 到 Wasm 工具链包含 libc、SDL、OpenGL 模拟、文件系统
wasi-sdk2019,WebAssembly Community Group基于 WASI 的最小工具链不依赖 Emscripten runtime;服务端友好
Clang/LLVM wasm32 target2017,LLVM 团队LLVM 原生 Wasm 后端编译器核心;其他工具链的底层
wasm-ldLLVM 子项目Wasm 链接器处理模块合并、符号解析
wabt (WebAssembly Binary Toolkit)W3C 社区组Wasm 工具集反汇编、验证、文本格式转换
BinaryenWebAssembly CommunityIR 与优化Emscripten 内部使用
wasmer / wasmtime2019–2020服务端 Wasm runtimeJIT/AOT;WASI 实现

形式化定义

WebAssembly 模块结构

Wasm 模块是 Wasm 二进制格式的基本部署单元,其顶层结构可形式化为:

Module::={types,funcs,tables,mems,globals,elems,datas,imports,exports,start,code}\text{Module} ::= \{ \text{types}, \text{funcs}, \text{tables}, \text{mems}, \text{globals}, \text{elems}, \text{datas}, \text{imports}, \text{exports}, \text{start}, \text{code} \}

各字段语义:

  • types:函数签名集合 {functypei}\{ \text{functype}_i \},每个签名形如 [t1∗]→[t2∗][t_1^*] \to [t_2^*];
  • funcs:函数定义集合,包括代码体;
  • tables:间接调用表,元素为 funcref 或 externref;
  • mems:线性内存定义,记为 Mem(n,m)\text{Mem}(n, m),最小 nn 页(每页 64KiB),最大 mm 页;
  • globals:全局变量,可变/不可变,带类型;
  • elems:表初始化数据;
  • datas:内存初始化数据;
  • imports:外部依赖(函数、表、内存、全局);
  • exports:对外暴露的符号;
  • start:模块实例化时立即调用的函数;
  • code:函数体的二进制编码。

线性内存的形式化语义

Wasm 的内存模型是单一连续的字节数组 M\mathcal{M},定义在地址空间 [0,232)[0, 2^{32})(wasm32)或 [0,264)[0, 2^{64})(wasm64):

M:Address→Byte,Address={0,1,…,L−1},L∈N\mathcal{M} : \text{Address} \to \text{Byte}, \quad \text{Address} = \{0, 1, \dots, L-1\}, \quad L \in \mathbb{N}

内存大小以”页”为单位,每页 64 KiB=6553664 \text{ KiB} = 65536 字节。memory.grow 指令可动态扩展内存:

grow(Δ):L′=L+Δ,M′=M∪{L…L′−1→0}\text{grow}(\Delta) : L' = L + \Delta, \quad \mathcal{M}' = \mathcal{M} \cup \{ L \dots L' - 1 \to 0 \}

C++ 中的指针 T* p 在 Wasm 中表示为整数偏移 offset∈N\text{offset} \in \mathbb{N},解引用 *p 翻译为 load 指令:

load(T,offset)=read T bytes from M[offset:offset+sizeof(T)]\text{load}(T, \text{offset}) = \text{read } T \text{ bytes from } \mathcal{M}[\text{offset} : \text{offset} + \text{sizeof}(T)]

这与 C++ 的内存模型形成直接对应:C++ 对象地址即为 Wasm 内存偏移。这是 C++/Wasm 互操作语义清晰的根本原因。

栈式虚拟机执行模型

Wasm 采用栈式执行模型:指令不显式指定操作数,而是从隐式操作数栈顶弹出。形式化:

Instr:Stack→Stack×Effect\text{Instr} : \text{Stack} \to \text{Stack} \times \text{Effect}

例如,加法指令 i32.add 的语义:

i32.add:(s::val1::val2::[])→(s::(val1+val2)::[])\text{i32.add} : (s :: \text{val}_1 :: \text{val}_2 :: []) \to (s :: (\text{val}_1 + \text{val}_2) :: [])

这种设计的好处:

  1. 紧凑编码:指令无需编码操作数位置;
  2. 验证简单:栈高度可在编译期静态确定;
  3. 优化友好:易于生成单静态赋值(SSA)形式供后续优化。

类型系统

Wasm 1.0 MVP 仅支持四种基本数值类型:

类型Wasm 名语义
32 位整数i32无符号/有符号共用,按指令解释
64 位整数i64同上
32 位浮点f32IEEE 754 single
64 位浮点f64IEEE 754 double

Wasm 2.0 扩展:

  • SIMD:v128,128 位向量,支持 4×i32、4×f32、2×i64、2×f64 等 lane 排列;
  • reference types:funcref、externref,函数引用与外部引用;
  • multi-value:函数可返回多个值。

Wasm GC 提案(2023)新增:

  • 结构类型:struct、array,由 GC 管理;
  • 数组类型:可变长度;
  • 函数类型:作为一等值;
  • downcast:运行期类型检查。

C++ 编译为 Wasm 时,仅使用 i32/i64/f32/f64/v128 与 funcref(用于函数表)。C++ 的堆对象由线性内存管理,不使用 Wasm GC 类型——这是 C++ 与 Java/Kotlin Wasm 编译的根本区别。

理论推导

推导 1:Wasm 性能上限的形式化分析

设 JS 引擎的 JIT 编译耗时 TJITT_{\text{JIT}}、解释执行每条指令耗时 tinterpt_{\text{interp}},C++ 编译为 Wasm 的字节码下载耗时 TdownloadT_{\text{download}}、解码耗时 TdecodeT_{\text{decode}}、执行每条指令耗时 twasmt_{\text{wasm}}。

对于程序 PP 含 NN 条指令:

  • JS 总耗时:TJS=TJIT+N⋅tinterpT_{\text{JS}} = T_{\text{JIT}} + N \cdot t_{\text{interp}}(若未触发 JIT)或 TJIT+N⋅toptT_{\text{JIT}} + N \cdot t_{\text{opt}}(若触发);
  • Wasm 总耗时:TWasm=Tdownload+Tdecode+N⋅twasmT_{\text{Wasm}} = T_{\text{download}} + T_{\text{decode}} + N \cdot t_{\text{wasm}}。

定理:当 NN 足够大时,TWasm<TJST_{\text{Wasm}} < T_{\text{JS}} 当且仅当:

twasm<tinterp−TJIT−Tdownload−TdecodeNt_{\text{wasm}} < t_{\text{interp}} - \frac{T_{\text{JIT}} - T_{\text{download}} - T_{\text{decode}}}{N}

实测数据(Chrome 120,2024):

  • tinterp≈5 nst_{\text{interp}} \approx 5\text{ ns},topt≈0.5 nst_{\text{opt}} \approx 0.5\text{ ns}(JIT 后);
  • twasm≈0.3 nst_{\text{wasm}} \approx 0.3\text{ ns};
  • TJIT≈50 msT_{\text{JIT}} \approx 50\text{ ms},Tdecode≈5 msT_{\text{decode}} \approx 5\text{ ms}。

当 N>108N > 10^8 时,Wasm 的优势显著。对于短脚本(N<106N < 10^6),JS 通常更快(启动开销主导)。

推导 2:线性内存与 GC 的对比

C++ 使用线性内存模型:

  • 优点:与 C++ 语义一致;指针运算直接;GC 不暂停执行;
  • 缺点:模块间无法共享数据结构;内存只能整体增长;泄漏检测困难。

Wasm GC 语言(Java/Kotlin)使用 GC 类型:

  • 优点:模块间可共享 GC 对象;自动内存管理;与其他 GC 语言互操作;
  • 缺点:与 C++ 语义不匹配;暂停时间影响实时性。

形式化对比:

Costlinear(P)=∑i=1Nload/storei⋅tmem\text{Cost}_{\text{linear}}(P) = \sum_{i=1}^{N} \text{load/store}_i \cdot t_{\text{mem}} CostGC(P)=∑i=1Naccessi⋅tgc-heap+TGC-pause\text{Cost}_{\text{GC}}(P) = \sum_{i=1}^{N} \text{access}_i \cdot t_{\text{gc-heap}} + T_{\text{GC-pause}}

对于 C++ 项目,T_{GC-pause} 通常不存在(除非嵌入 V8 GC),但 load/store 指令数量与原生代码相同。Wasm GC 的 struct.get 等指令可能比线性内存 i32.load 更慢(需要类型检查)。

推导 3:Wasm 安全边界的证明

Wasm 的安全模型基于沙箱执行:

  1. 隔离性:Wasm 模块只能通过导出的函数与导入的接口与外界交互;
  2. 内存隔离:每个模块拥有独立的线性内存,无法直接访问其他模块或浏览器内存;
  3. 控制流完整性:所有间接调用通过函数表索引,编译期验证签名;
  4. 资源限制:内存大小、表大小、调用栈深度有上限。

形式化:设模块 MM 的导入接口集为 II,导出接口集为 EE。则 MM 的可观察行为 Obs(M)\text{Obs}(M) 满足:

Obs(M)⊆σ(I)∪ρ(E)\text{Obs}(M) \subseteq \sigma(I) \cup \rho(E)

其中 σ\sigma 是 II 上的语义闭包(模块只能调用导入的函数),ρ\rho 是 EE 上的可达性闭包(外部只能观察导出的函数)。这意味着模块无法读取或修改其内存沙箱外的任何状态,除非通过显式提供的导入函数。

推导 4:Wasm 与原生代码的性能差距

Wasm 与原生代码的差距主要来自:

  1. 不能 SIMD 全覆盖:原生 AVX-512 有 512 位寄存器,Wasm SIMD 仅 128 位(4×f32);
  2. 调用约定开销:Wasm 与 JS 互操作需要类型转换与跨边界检查;
  3. 内存模型限制:Wasm 1.0 仅 32 位地址,最大 4GB;
  4. JIT 优化深度:浏览器 JIT 优化窗口通常比 native LLVM -O3 浅。

实测数据(SpecCPU 2017,SQLite,Ffmpeg,2024):

工作负载原生 (-O3)Wasm (-O3)差距
SQLite OLTP1.0×1.05×5%
Eigen 矩阵乘1.0×1.15×15%
Ffmpeg H.264 解码1.0×1.3×30%
Polaris 物理1.0×1.5×50%(无 AVX)
密码学 SHA-2561.0×1.1×10%

差距主要来自 SIMD 宽度与缺少 AVX/AVX-512。对于 SIMD 重的工作负载,建议启用 wasm_simd128。

基础概念

Emscripten 工具链

Emscripten 是把 C/C++ 编译为 Wasm 的主流工具链,其核心组件:

  • emcc / em++:编译器驱动,封装 clang + LLVM + wasm-ld + Binaryen;
  • Emscripten-Linker:处理 C++ 标准库、系统调用模拟;
  • embind:C++ ↔ JavaScript 绑定生成器;
  • WebIDL Binder:另一种绑定方式(旧式);
  • file packager:将本地文件打包为虚拟文件系统;
  • system libraries:libc++、libc 兼容层、SDL2、GL、AL 等。

编译目标

Emscripten 支持多种输出格式:

输出命令用途
a.out.js + a.out.wasmemcc main.cpp默认;JS 胶水代码 + Wasm
a.out.wasm onlyemcc main.cpp -o a.out.wasm -s STANDALONE_WASM纯 Wasm(WASI 兼容)
a.out.htmlemcc main.cpp -o a.out.html自包含 HTML Demo
a.out.mjsemcc main.cpp -o a.out.mjsES Module 输出

Wasm 文本格式(WAT)

Wasm 有二进制格式(.wasm)与文本格式(.wat)。文本格式用 S-表达式描述:

(module
  (func $add (export "add") (param i32 i32) (result i32)
    local.get 0
    local.get 1
    i32.add))

对应二进制约 8 字节。文本格式用于调试、阅读、手写测试。

WASI(WebAssembly System Interface)

WASI 是面向服务端的 Wasm 系统接口,定义了文件、网络、时钟、随机数等 POSIX-like API。WASI 的设计原则是能力安全(capability-based security):模块不能自由访问文件系统,必须通过显式授权的目录描述符。

# 用 wasi-sdk 编译
clang --target=wasm32-wasi --sysroot=/path/to/wasi-sysroot main.cpp -o main.wasm

# 用 wasmtime 运行
wasmtime main.wasm

WASI 与 Emscripten 的对比:

维度EmscriptenWASI
目标浏览器为主服务端、边缘、嵌入式
文件系统完整模拟(MEMFS)能力安全(preopened dirs)
网络通过 JS 桥接socket API(WASI sockets)
多线程pthreads + SharedArrayBufferpthreads(部分 runtime)
启动开销较大(含 JS runtime)较小
标准化厂商事实W3C 标准化

代码示例

示例 1:最小化 “Hello, WebAssembly”

// hello.cpp
#include <iostream>

int main() {
    std::cout << "Hello, WebAssembly!" << std::endl;
    return 0;
}

编译:

emcc hello.cpp -o hello.js
node hello.js
# 输出: Hello, WebAssembly!

或生成 HTML:

emcc hello.cpp -o hello.html
# 用浏览器打开 hello.html

示例 2:使用 embind 暴露 C++ 类给 JavaScript

// calculator.cpp
#include <emscripten/bind.h>
#include <string>
#include <vector>

/// 计算器类:演示 embind 绑定
class Calculator {
public:
    Calculator() : value_(0) {}

    /// 加法
    double add(double x) {
        value_ += x;
        return value_;
    }

    /// 减法
    double sub(double x) {
        value_ -= x;
        return value_;
    }

    /// 获取当前值
    double get() const { return value_; }

    /// 重置
    void reset() { value_ = 0; }

    /// 处理数组:计算平均值
    double average(const std::vector<double>& vec) const {
        if (vec.empty()) return 0.0;
        double sum = 0;
        for (auto x : vec) sum += x;
        return sum / vec.size();
    }

    /// 返回字符串
    std::string describe() const {
        return "Calculator(value=" + std::to_string(value_) + ")";
    }

private:
    double value_;
};

/// 自由函数
int factorial(int n) {
    if (n <= 1) return 1;
    return n * factorial(n - 1);
}

/// 注册 embind 绑定
EMSCRIPTEN_BINDINGS(calculator_module) {
    emscripten::class_<Calculator>("Calculator")
        .constructor<>()
        .function("add", &Calculator::add)
        .function("sub", &Calculator::sub)
        .function("get", &Calculator::get)
        .function("reset", &Calculator::reset)
        .function("average", &Calculator::average)
        .function("describe", &Calculator::describe)
        ;

    emscripten::function("factorial", &factorial);

    // 注册 vector<double> 类型
    emscripten::register_vector<double>("VectorDouble");
}

编译:

emcc calculator.cpp -o calculator.js \
    --bind \
    -s MODULARIZE=1 \
    -s EXPORT_NAME=createCalculatorModule \
    -s EXPORT_ES6=1 \
    -O3

JavaScript 调用:

import createCalculatorModule from './calculator.js';

const Module = await createCalculatorModule();

const calc = new Module.Calculator();
calc.add(10);
calc.add(5);
console.log(calc.get());     // 15
console.log(calc.describe()); // "Calculator(value=15)"

const vec = new Module.VectorDouble();
vec.push_back(1.0);
vec.push_back(2.0);
vec.push_back(3.0);
console.log(calc.average(vec)); // 2.0

console.log(Module.factorial(5)); // 120

示例 3:使用 Emscripten 的 EM_ASM 内联 JavaScript

#include <emscripten/em_asm.h>
#include <iostream>

void log_to_js(const std::string& msg) {
    // EM_ASM 宏允许在 C++ 中内联 JavaScript
    EM_ASM({
        console.log("From C++: " + UTF8ToString($0));
    }, msg.c_str());
}

int main() {
    log_to_js("Hello from C++ via EM_ASM!");
    return 0;
}

EM_ASM 适合快速调试;生产代码应使用 embind。

示例 4:使用线性内存(直接操作 Wasm 内存)

#include <emscripten/emscripten.h>
#include <cstdint>
#include <vector>

/// 在线性内存中分配数组并暴露给 JS
EMSCRIPTEN_KEEPALIVE
float* create_float_array(int n) {
    return new float[n];
}

EMSCRIPTEN_KEEPALIVE
void fill_sine(float* arr, int n, float freq) {
    for (int i = 0; i < n; ++i) {
        arr[i] = std::sin(2 * M_PI * freq * i / n);
    }
}

EMSCRIPTEN_KEEPALIVE
void free_float_array(float* arr) {
    delete[] arr;
}

JavaScript 端通过 Module._create_float_array 返回的指针操作 Wasm 内存:

const n = 1024;
const ptr = Module._create_float_array(n);
Module._fill_sine(ptr, n, 440.0);

// 通过 HEAPF8 直接访问 Wasm 线性内存
const samples = new Float32Array(Module.HEAPF8.buffer, ptr, n);
for (let i = 0; i < 10; ++i) {
    console.log(samples[i]);
}

Module._free_float_array(ptr);

示例 5:使用 SIMD 加速矩阵乘法

#include <wasm_simd128.h>
#include <emscripten/emscripten.h>

/// 4x4 矩阵乘法,使用 Wasm SIMD
EMSCRIPTEN_KEEPALIVE
void matmul4x4(const float* A, const float* B, float* C) {
    // 加载 B 的列到 SIMD 寄存器
    v128_t b0 = wasm_v128_load(B + 0);
    v128_t b1 = wasm_v128_load(B + 4);
    v128_t b2 = wasm_v128_load(B + 8);
    v128_t b3 = wasm_v128_load(B + 12);

    for (int i = 0; i < 4; ++i) {
        // 加载 A 的第 i 行,广播
        v128_t a0 = wasm_f32x4_splat(A[i*4 + 0]);
        v128_t a1 = wasm_f32x4_splat(A[i*4 + 1]);
        v128_t a2 = wasm_f32x4_splat(A[i*4 + 2]);
        v128_t a3 = wasm_f32x4_splat(A[i*4 + 3]);

        // 计算 C 的第 i 行
        v128_t c = wasm_f32x4_add(
            wasm_f32x4_add(
                wasm_f32x4_mul(a0, b0),
                wasm_f32x4_mul(a1, b1)
            ),
            wasm_f32x4_add(
                wasm_f32x4_mul(a2, b2),
                wasm_f32x4_mul(a3, b3)
            )
        );
        wasm_v128_store(C + i*4, c);
    }
}

编译时启用 SIMD:

emcc matmul.cpp -o matmul.js \
    -msimd128 \
    -O3 \
    -s EXPORTED_FUNCTIONS=['_matmul4x4'] \
    -s EXPORTED_RUNTIME_METHODS=['ccall','cwrap']

示例 6:使用 pthreads 进行多线程 Wasm

#include <pthread.h>
#include <emscripten/emscripten.h>
#include <vector>
#include <iostream>

struct WorkerData {
    int id;
    int result;
};

void* worker(void* arg) {
    WorkerData* data = static_cast<WorkerData*>(arg);
    // 模拟计算
    int sum = 0;
    for (int i = 0; i < 1000000; ++i) {
        sum += i * data->id;
    }
    data->result = sum;
    return nullptr;
}

int main() {
    const int N = 4;
    std::vector<pthread_t> threads(N);
    std::vector<WorkerData> data(N);

    for (int i = 0; i < N; ++i) {
        data[i] = {i + 1, 0};
        pthread_create(&threads[i], nullptr, worker, &data[i]);
    }

    for (int i = 0; i < N; ++i) {
        pthread_join(threads[i], nullptr);
        std::cout << "Worker " << i << " result: " << data[i].result << "\n";
    }

    return 0;
}

编译时启用 pthreads:

emcc threads.cpp -o threads.js \
    -pthread \
    -s PTHREAD_POOL_SIZE=4 \
    -s ALLOW_BLOCKING_ON_MAIN_THREAD=1

注意:浏览器要求 Cross-Origin-Opener-Policy: same-origin 与 Cross-Origin-Embedder-Policy: require-corp 头才能使用 SharedArrayBuffer。

示例 7:使用文件系统(MEMFS)

#include <emscripten/emscripten.h>
#include <fstream>
#include <iostream>

int main() {
    // 写入虚拟文件系统
    std::ofstream out("/tmp/test.txt");
    out << "Hello from MEMFS!";
    out.close();

    // 读取
    std::ifstream in("/tmp/test.txt");
    std::string content((std::istreambuf_iterator<char>(in)),
                         std::istreambuf_iterator<char>());
    std::cout << content << "\n";

    return 0;
}

JavaScript 端可以预加载文件到 MEMFS:

Module.preRun = () => {
    FS.writeFile('/data/input.txt', 'some content');
};

或使用 --preload-file 打包:

emcc fs.cpp -o fs.js --preload-file ./assets/data.txt

对比分析

Wasm vs JavaScript vs 原生代码

维度JavaScriptWebAssemblyC++ Native
启动延迟极快(解析即执行)中(需下载+解码)极快(直接执行)
性能(峰值)1×1.1–1.5×1.0×
性能(冷启动)0.5–1×1.1×1.0×
内存访问GC 受限直接线性内存直接
类型安全弱(动态)强(静态验证)强(编译期)
调试体验极佳(DevTools)中(source map)佳(gdb/lldb)
生态复用npm 海量沿用 C++ 生态沿用 C++ 生态
部署体积KB–MBKB–MB(同 C++)取决于链接
跨平台浏览器全平台浏览器+WASIOS/ABI 限制
安全沙箱是是否

Emscripten vs WASI vs Native

维度EmscriptenWASINative
目标浏览器+Node.js服务端OS 直接执行
文件系统MEMFS 模拟能力安全直接访问
网络JS 桥接WASI socketssocket
GUICanvas/WebGL/HTML无任意
启动开销较大较小最小
兼容性主流浏览器wasmtime/wasmer与 OS 绑定
标准化事实标准W3C 标准化—

工具链对比

工具链输出目标标准库适用场景
Emscripten浏览器 + Node.js完整 libc++ + Web APIWeb 应用
wasi-sdk服务端(WASI)WASI-libc服务端、CLI
Clang --target=wasm32-unknown-unknown无 OS无嵌入式、最小模块
Cheerp浏览器子集商业支持
AssemblyScript浏览器自带TypeScript 风格

常见陷阱

陷阱 1:未启用 SIMD 导致性能回退

# 错误:未加 -msimd128,SIMD 内联函数被忽略
emcc matmul.cpp -o matmul.js

必须显式启用:

emcc matmul.cpp -o matmul.js -msimd128

陷阱 2:未设置 COOP/COEP 头导致 SharedArrayBuffer 不可用

浏览器要求以下 HTTP 响应头才能使用 SharedArrayBuffer(多线程 Wasm 必需):

Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp

若缺失,new SharedArrayBuffer(...) 抛 ReferenceError,pthread 创建失败。

陷阱 3:未限制内存增长导致 OOM

#include <vector>
int main() {
    std::vector<int> v;
    while (true) v.push_back(0);  // 无限增长
}

Wasm 默认最大内存 4GB(wasm32)。可以通过 -s MAXIMUM_MEMORY 调整,但有限制:

emcc grow.cpp -o grow.js -s MAXIMUM_MEMORY=1GB -s ALLOW_MEMORY_GROWTH=1

陷阱 4:C++ 异常默认关闭

Emscripten 默认禁用异常以减小体积:

# 默认:禁用异常(throw 调用 std::terminate)
emcc foo.cpp -o foo.js

# 启用异常
emcc foo.cpp -o foo.js -fexceptions

陷阱 5:RTTI 默认关闭

dynamic_cast 与 typeid 默认不可用:

emcc foo.cpp -o foo.js -fno-rtti  # 默认
emcc foo.cpp -o foo.js -frtti     # 显式启用

启用 RTTI 会增加约 100–500 KB 体积。

陷阱 6:跨边界传递指针的整数类型

C++ 指针在 wasm32 下是 32 位,但 JavaScript Number 是双精度浮点。直接传递大指针(>2^53)会丢失精度。wasm32 下指针最大 4GB(<2^32),但 JS 需用 >>>0 强制无符号解释:

// 错误:可能为负数
const ptr = Module._malloc(-1);

// 正确:强制无符号
const ptr = (Module._malloc(n) >>> 0);

陷阱 7:asyncify 增大体积并影响性能

为支持 async/await 风格的 C++ 代码(如 emscripten_sleep),需启用 asyncify:

emcc async.cpp -o async.js -s ASYNCIFY=1

asyncify 会使模块体积增加 2–3 倍,性能下降 20–50%。仅在必要时启用。

陷阱 8:动态加载模块体积爆炸

Wasm 不支持原生动态链接(除非使用 dlopen 模拟)。如果代码分多个模块,最终会被静态链接到一个大文件中。对于库代码,可以使用 -sSIDE_MODULE 与主模块分离,但需要 dlopen 显式加载。

工程实践

实践 1:CMake 集成 Emscripten

# CMakeLists.txt
cmake_minimum_required(VERSION 3.13)
project(wasm_demo CXX)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

if(EMSCRIPTEN)
    # Emscripten 特定选项
    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O3 -msimd128")
    set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} \
        -s MODULARIZE=1 \
        -s EXPORT_NAME=createModule \
        -s EXPORT_ES6=1 \
        -s ALLOW_MEMORY_GROWTH=1 \
        --bind")
endif()

add_executable(wasm_demo src/main.cpp)

if(EMSCRIPTEN)
    set_target_properties(wasm_demo PROPERTIES
        SUFFIX ".mjs"
        RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/dist")
endif()

使用 emcmake 包装:

mkdir build && cd build
emcmake cmake -DCMAKE_BUILD_TYPE=Release ..
emmake make

实践 2:使用 embind 暴露枚举与智能指针

#include <emscripten/bind.h>
#include <memory>

enum class Color { Red, Green, Blue };

class Shape {
public:
    virtual ~Shape() = default;
    virtual double area() const = 0;
};

class Circle : public Shape {
public:
    Circle(double r) : r_(r) {}
    double area() const override { return 3.14159 * r_ * r_; }
private:
    double r_;
};

EMSCRIPTEN_BINDINGS(shapes) {
    emscripten::enum_<Color>("Color")
        .value("Red", Color::Red)
        .value("Green", Color::Green)
        .value("Blue", Color::Blue)
        ;

    emscripten::class_<Shape>("Shape")
        .smart_ptr<std::shared_ptr<Shape>>("shared_ptr<Shape>")
        .function("area", &Shape::area)
        ;

    emscripten::class_<Circle, emscripten::base<Shape>>("Circle")
        .smart_ptr_constructor<std::shared_ptr<Circle>, double>("shared_ptr<Circle>", emscripten::args<double>())
        ;
}

实践 3:体积优化策略

# 最小化体积
emcc main.cpp -o main.js \
    -Oz \                          # 体积优先
    -flto \                        # 链接时优化
    -s FILESYSTEM=0 \             # 禁用文件系统(节省 ~50KB)
    -s ENVIRONMENT=web \           # 仅 Web 环境
    -s MODULARIZE=1 \
    -s EXPORT_ES6=1 \
    -s SINGLE_FILE=1 \            # 内联 Wasm 为 base64(牺牲大小换简化部署)
    -s DYNAMIC_EXECUTION=0 \       # 禁用 eval(CSP 友好)
    --closure 1 \                  # JS 压缩
    --closure-args="--compilation_level=ADVANCED_OPTIMIZATIONS" \
    --llvm-lto 2                   # LLVM LTO

实测对比(计算密集型库,1MB 源码):

优化级别体积
-O01.2 MB
-O2350 KB
-O3320 KB
-Oz280 KB
-Oz + LTO + Closure180 KB

实践 4:使用 --profiling 保留调试符号

emcc main.cpp -o main.js --profiling -O2
# 生成的 main.wasm 保留函数名,便于 DevTools 调试

实践 5:源代码映射(source map)

emcc main.cpp -o main.js -gsource-map -O2
# 生成 main.wasm + main.wasm.map
# 浏览器 DevTools 会显示原始 C++ 代码

实践 6:使用 wasm-opt 进一步优化

emcc main.cpp -o main.js -O2
wasm-opt -O4 main.wasm -o main.opt.wasm
# wasm-opt 是 Binaryen 工具,进一步优化 Wasm 字节码

实践 7:CI/CD 流水线(GitHub Actions)

# .github/workflows/build.yml
name: Build Wasm

on: [push, pull_request]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Setup Emscripten
        uses: mymindstorm/setup-emsdk@v12
        with:
          version: 3.1.45
      - name: Build
        run: |
          mkdir build && cd build
          emcmake cmake -DCMAKE_BUILD_TYPE=Release ..
          emmake make
      - name: Upload artifacts
        uses: actions/upload-artifact@v3
        with:
          name: wasm-build
          path: build/dist/

实践 8:使用 NPM 发布

// package.json
{
  "name": "@myorg/wasm-lib",
  "version": "1.0.0",
  "main": "dist/wasm_lib.mjs",
  "types": "dist/wasm_lib.d.ts",
  "files": ["dist"],
  "scripts": {
    "build": "emcmake cmake -B build && cmake --build build",
    "test": "node test/test.js"
  }
}

TypeScript 类型定义文件:

// dist/wasm_lib.d.ts
export interface Calculator {
    add(x: number): number;
    sub(x: number): number;
    get(): number;
    reset(): void;
    describe(): string;
}

export interface Module {
    Calculator: new () => Calculator;
    factorial(n: number): number;
}

export default function createModule(): Promise<Module>;

案例研究

案例 1:将 OpenCV 编译为 Wasm

git clone https://github.com/opencv/opencv.git
cd opencv
mkdir build && cd build

emcmake cmake -DCMAKE_BUILD_TYPE=Release \
    -DBUILD_SHARED_LIBS=OFF \
    -DBUILD_opencv_world=ON \
    -DWITH_JASPER=OFF \
    -DWITH_OPENJPEG=OFF \
    -DBUILD_TESTS=OFF \
    -DBUILD_PERF_TESTS=OFF \
    -DOPENCV_EXTRA_EXE_LINKER_FLAGS="--bind -s MODULARIZE=1 -s EXPORT_NAME=createOpenCV" \
    ..

emmake make -j8

JavaScript 调用:

import createOpenCV from './opencv.js';

const cv = await createOpenCV();

const img = cv.imread('canvas');
const gray = new cv.Mat();
cv.cvtColor(img, gray, cv.COLOR_RGBA2GRAY);
cv.imshow('output', gray);
img.delete();
gray.delete();

案例 2:将 SQLite 编译为 WASI 应用

git clone https://github.com/sqlite/sqlite.git
cd sqlite

# 下载 wasi-sdk
wget https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-20/wasi-sdk-20.0-linux.tar.gz
tar xf wasi-sdk-20.0-linux.tar.gz

# 配置并编译
./configure --host=wasm32-wasi \
    CC=$PWD/wasi-sdk-20.0/bin/clang \
    CXX=$PWD/wasi-sdk-20.0/bin/clang++ \
    --disable-readline \
    --disable-threadsafe
make

# 运行
wasmtime sqlite3

案例 3:物理仿真(Box2D)

// box2d_demo.cpp
#include <box2d/box2d.h>
#include <emscripten/bind.h>

class PhysicsWorld {
public:
    PhysicsWorld() {
        b2WorldDef worldDef = b2DefaultWorldDef();
        worldDef.gravity = {0.0f, -9.8f};
        world_ = b2CreateWorld(&worldDef);
    }

    ~PhysicsWorld() {
        b2DestroyWorld(world_);
    }

    b2BodyId create_ground(float x, float y, float w, float h) {
        b2BodyDef bodyDef = b2DefaultBodyDef();
        bodyDef.position = {x, y};
        b2BodyId ground = b2CreateBody(world_, &bodyDef);
        b2Polygon box = b2MakeBox(w / 2, h / 2);
        b2CreatePolygonShape(ground, &b2DefaultShapeDef(), &box);
        return ground;
    }

    b2BodyId create_box(float x, float y, float w, float h) {
        b2BodyDef bodyDef = b2DefaultBodyDef();
        bodyDef.type = b2_dynamicBody;
        bodyDef.position = {x, y};
        b2BodyId body = b2CreateBody(world_, &bodyDef);
        b2Polygon box = b2MakeBox(w / 2, h / 2);
        b2CreatePolygonShape(body, &b2DefaultShapeDef(), &box);
        return body;
    }

    void step(float dt) {
        b2World_Step(world_, dt, 4);
    }

private:
    b2WorldId world_;
};

EMSCRIPTEN_BINDINGS(box2d) {
    emscripten::class_<PhysicsWorld>("PhysicsWorld")
        .constructor<>()
        .function("createGround", &PhysicsWorld::create_ground)
        .function("createBox", &PhysicsWorld::create_box)
        .function("step", &PhysicsWorld::step)
        ;
}

案例 4:端到端 React + Wasm 集成

// App.jsx
import { useEffect, useRef, useState } from 'react';
import createCalculatorModule from './calculator.mjs';

export default function App() {
    const [Module, setModule] = useState(null);
    const [result, setResult] = useState(0);
    const calcRef = useRef(null);

    useEffect(() => {
        createCalculatorModule().then(setModule);
    }, []);

    const handleAdd = () => {
        if (!Module) return;
        if (!calcRef.current) calcRef.current = new Module.Calculator();
        const newVal = calcRef.current.add(1);
        setResult(newVal);
    };

    return (
        <div>
            <h1>Wasm Calculator</h1>
            <p>Value: {result}</p>
            <button onClick={handleAdd} disabled={!Module}>
                Add 1
            </button>
        </div>
    );
}

案例 5:用 Wasm 实现密码学(SHA-256)

// sha256.cpp
#include <emscripten/bind.h>
#include <array>
#include <cstdint>
#include <string>

namespace {

constexpr std::array<uint32_t, 64> K = {
    0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5,
    // ... 完整 K 数组 ...
};

class SHA256 {
public:
    std::string hash(const std::string& input) {
        // SHA-256 实现
        // ... 详细实现 ...
        return "abc123";  // 占位
    }
};

}

EMSCRIPTEN_BINDINGS(crypto) {
    emscripten::class_<SHA256>("SHA256")
        .constructor<>()
        .function("hash", &SHA256::hash)
        ;
}

JavaScript 调用:

import createCryptoModule from './sha256.mjs';
const crypto = await createCryptoModule();
const sha = new crypto.SHA256();
console.log(sha.hash('hello world'));

案例 6:图像处理流水线(Eigen + SIMD)

#include <emscripten/bind.h>
#include <wasm_simd128.h>
#include <vector>
#include <cmath>

/// 使用 Wasm SIMD 加速的图像卷积
EMSCRIPTEN_KEEPALIVE
void convolve3x3_simd(const float* input, float* output,
                       int width, int height,
                       const float kernel[9]) {
    for (int y = 1; y < height - 1; ++y) {
        for (int x = 1; x < width - 1; x += 4) {
            // SIMD 处理 4 个像素
            v128_t sum = wasm_f32x4_splat(0.0f);
            for (int ky = -1; ky <= 1; ++ky) {
                for (int kx = -1; kx <= 1; ++kx) {
                    v128_t pixels = wasm_v128_load(
                        input + (y + ky) * width + (x + kx));
                    v128_t k = wasm_f32x4_splat(
                        kernel[(ky+1)*3 + (kx+1)]);
                    sum = wasm_f32x4_add(sum, wasm_f32x4_mul(pixels, k));
                }
            }
            wasm_v128_store(output + y * width + x, sum);
        }
    }
}

EMSCRIPTEN_BINDINGS(image_processor) {
    emscripten::function("convolve3x3", &convolve3x3_simd,
        emscripten::allow_raw_pointers());
}

性能分析

实测基准:FFT

// 基准:1M 点 FFT
// 原生 -O3: 120 ms
// Wasm -O3: 130 ms
// Wasm -O3 + SIMD: 95 ms(比原生快,因 SIMD 优化更激进)
// JavaScript: 1100 ms(10x 慢)

实测基准:物理仿真

// Box2D 1000 个刚体 60 FPS
// 原生 -O3: 4 ms/frame
// Wasm -O3: 5 ms/frame
// Wasm -O3 + threads (4 workers): 1.5 ms/frame
// JavaScript: 30 ms/frame

实测基准:SQLite 查询

// 10000 条 INSERT
// 原生: 80 ms
// Wasm (WASI): 85 ms
// Wasm (Emscripten): 120 ms(FS 模拟开销)
// IndexedDB JS: 1500 ms

启动时间分析

Wasm 模块启动开销包括:

  1. 下载时间(受网络与压缩影响);
  2. 解码时间(约 5–20 ms / MB);
  3. 编译时间(约 50–200 ms / MB,Chrome Streaming 编译);
  4. 实例化时间(约 5–30 ms,含全局初始化)。

可通过 WebAssembly.instantiateStreaming 实现流式编译(边下载边编译)。

与现代 C++ 特性的兼容性

标准 C++ 库支持

Emscripten 提供完整的 libc++ 与 libc 兼容层:

  • <vector>、<string>、<map>、<unordered_map>:完整支持;
  • <thread>、<mutex>、<future>:通过 pthreads 模拟;
  • <filesystem>:通过 MEMFS 模拟;
  • <regex>:完整支持(但体积大);
  • <random>:使用浏览器 PRNG 注入;
  • <chrono>:使用 performance.now();
  • <iostream>:通过 JS console 输出。

C++20/23/26 特性

  • 协程:支持(C++20);
  • 概念:支持(C++20);
  • 模块:部分支持(建议测试);
  • std::format:支持(C++20/23);
  • std::expected:支持(C++23);
  • 反射:尚不支持(C++26 草案)。

异常与 RTTI

# 默认:禁用异常 + 禁用 RTTI(最小体积)
emcc foo.cpp -o foo.js

# 启用异常
emcc foo.cpp -o foo.js -fexceptions

# 启用 RTTI
emcc foo.cpp -o foo.js -frtti

# 同时启用(最大兼容性)
emcc foo.cpp -o foo.js -fexceptions -frtti

实测体积影响(典型项目):

配置体积
默认(无异常/RTTI)100 KB
仅 RTTI250 KB
仅异常200 KB
异常 + RTTI350 KB

移动语义与 RAII

C++ 的移动语义与 RAII 在 Wasm 中完全有效。std::unique_ptr 在 wasm32 下仅占 4 字节(指针大小)。

与 WASI 的协同

WASI 组件模型(component model)

WASI 0.2 引入组件模型(component model),允许不同语言编写的 Wasm 模块互操作:

# 编译 C++ 为组件
clang --target=wasm32-wasi --sysroot=wasi-sysroot foo.cpp -o foo.wasm
wasm-tools component new foo.wasm -o foo.component.wasm

WIT(Wasm Interface Type)

WIT 用于描述组件接口:

// math.wit
package example:math;

interface calculator {
    add: func(a: f64, b: f64) -> f64;
    sub: func(a: f64, b: f64) -> f64;
}

陷阱与限制

限制 1:wasm32 地址空间限制

wasm32 最大 4GB 线性内存。若需更多,使用 wasm64(实验性,浏览器支持有限):

emcc foo.cpp -o foo.js -s MEMORY64=1

限制 2:无法直接访问 DOM

Wasm 不能直接调用 document.getElementById 等浏览器 API,必须通过 JS 桥接:

#include <emscripten/em_asm.h>

void set_title(const char* title) {
    EM_ASM({
        document.title = UTF8ToString($0);
    }, title);
}

限制 3:调试信息庞大

-g 编译会包含完整 DWARF 调试信息,模块可能数十 MB:

# 调试构建
emcc foo.cpp -g -o foo.js

# 生产构建
emcc foo.cpp -O3 -o foo.js

# 平衡:保留函数名
emcc foo.cpp --profiling -O2 -o foo.js

限制 4:跨边界异常不能传播到 JS

EMSCRIPTEN_BINDINGS(foo) {
    emscripten::function("throws", []() -> void {
        throw std::runtime_error("boom");
    });
}
try {
    Module.throws();
} catch (e) {
    // 默认下异常被吞,调用 std::terminate
    // 必须启用 -fexceptions 才能传播
}

工程实践

实践 1:使用 MODULARIZE 与 EXPORT_NAME

emcc main.cpp -o main.mjs \
    -s MODULARIZE=1 \
    -s EXPORT_NAME=createMyModule \
    -s EXPORT_ES6=1

这样模块可作为 ES Module 导入,便于现代前端工具链集成。

实践 2:使用 --preload-file 打包资源

emcc app.cpp -o app.js --preload-file ./assets/
# 生成 app.js, app.wasm, app.data

实践 3:使用 Module.preRun 初始化

const Module = {
    preRun: () => {
        FS.mkdir('/data');
        FS.writeFile('/data/config.json', '{}');
    },
    onRuntimeInitialized: () => {
        console.log('Wasm ready');
    }
};

实践 4:使用 cwrap 简化 C 函数调用

// 在 C++ 中用 extern "C" 导出
// extern "C" EMSCRIPTEN_KEEPALIVE int add(int a, int b) { return a + b; }

const add = Module.cwrap('add', 'number', ['number', 'number']);
console.log(add(2, 3));  // 5

实践 5:性能分析

# 生成性能分析数据
emcc main.cpp -o main.js --profiling -O2
# 在 Chrome DevTools Performance 标签中录制
# 查看 Wasm 函数耗时

实践 6:体积分析

# 生成体积报告
emcc main.cpp -o main.js -Oz --emit-symbol-map
# 查看 main.wasm.map 文件了解各符号占比

# 使用 twiggy 进一步分析
twiggy top main.wasm

案例研究:完整项目

项目:在线 Photoshop(图像处理)

// image_processor.cpp
#include <emscripten/bind.h>
#include <vector>
#include <cmath>

class ImageProcessor {
public:
    /// 高斯模糊
    std::vector<uint8_t> gaussian_blur(
        const std::vector<uint8_t>& input,
        int width, int height, float sigma) {

        std::vector<uint8_t> output(input.size(), 0);
        int radius = static_cast<int>(sigma * 3);

        // 生成高斯核
        std::vector<float> kernel(2 * radius + 1);
        float sum = 0;
        for (int i = -radius; i <= radius; ++i) {
            float v = std::exp(-(i*i) / (2 * sigma * sigma));
            kernel[i + radius] = v;
            sum += v;
        }
        for (auto& k : kernel) k /= sum;

        // 水平模糊
        #pragma omp parallel for
        for (int y = 0; y < height; ++y) {
            for (int x = 0; x < width; ++x) {
                float r = 0, g = 0, b = 0, a = 0;
                for (int k = -radius; k <= radius; ++k) {
                    int px = std::clamp(x + k, 0, width - 1);
                    int idx = (y * width + px) * 4;
                    float w = kernel[k + radius];
                    r += input[idx] * w;
                    g += input[idx+1] * w;
                    b += input[idx+2] * w;
                    a += input[idx+3] * w;
                }
                int idx = (y * width + x) * 4;
                output[idx] = static_cast<uint8_t>(r);
                output[idx+1] = static_cast<uint8_t>(g);
                output[idx+2] = static_cast<uint8_t>(b);
                output[idx+3] = static_cast<uint8_t>(a);
            }
        }

        return output;
    }
};

EMSCRIPTEN_BINDINGS(image) {
    emscripten::class_<ImageProcessor>("ImageProcessor")
        .constructor<>()
        .function("gaussianBlur", &ImageProcessor::gaussian_blur)
        ;
    emscripten::register_vector<uint8_t>("VectorUint8");
}

项目:游戏引擎(简化)

// game_engine.cpp
#include <emscripten.h>
#include <emscripten/bind.h>
#include <chrono>
#include <vector>

class GameEngine {
public:
    GameEngine(int width, int height)
        : width_(width), height_(height), canvas_(width * height * 4, 0) {}

    void update() {
        auto now = std::chrono::steady_clock::now();
        float dt = std::chrono::duration<float>(now - last_time_).count();
        last_time_ = now;

        // 更新游戏状态
        for (int i = 0; i < width_ * height_; ++i) {
            canvas_[i*4] = (canvas_[i*4] + 1) & 0xFF;       // R
            canvas_[i*4+1] = (canvas_[i*4+1] + 2) & 0xFF;   // G
            canvas_[i*4+2] = (canvas_[i*4+2] + 3) & 0xFF;   // B
            canvas_[i*4+3] = 255;                            // A
        }
    }

    emscripten::val get_canvas_data() {
        return emscripten::val(
            emscripten::typed_memory_view(canvas_.size(), canvas_.data()));
    }

private:
    int width_, height_;
    std::vector<uint8_t> canvas_;
    std::chrono::steady_clock::time_point last_time_ = std::chrono::steady_clock::now();
};

EMSCRIPTEN_BINDINGS(game) {
    emscripten::class_<GameEngine>("GameEngine")
        .constructor<int, int>()
        .function("update", &GameEngine::update)
        .function("getCanvasData", &GameEngine::get_canvas_data)
        ;
}

JavaScript 端:

import createGameModule from './game.mjs';

const Module = await createGameModule();
const engine = new Module.GameEngine(800, 600);

const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');

function loop() {
    engine.update();
    const data = new Uint8ClampedArray(engine.getCanvasData());
    ctx.putImageData(new ImageData(data, 800, 600), 0, 0);
    requestAnimationFrame(loop);
}
loop();

与 C++26 新特性的协同

静态反射(P2996)

C++26 静态反射可用于自动生成 embind 绑定:

// C++26 草案(未来)
#include <meta>
#include <emscripten/bind.h>

template <typename T>
void auto_bind() {
    constexpr auto info = ^^T;
    // 遍历成员函数,自动注册
    // ... 实验性 ...
}

契约编程(P2900)

// C++26 草案
[[pre: width > 0 && height > 0]]
GameEngine(int width, int height);

基础题

Q1:什么是 WebAssembly 的”线性内存”?为什么 C++ 与之语义契合?

Q2:使用 Emscripten 编译以下 C++ 代码,生成可在浏览器运行的 HTML:

#include <iostream>
int main() { std::cout << "Hello, Web!"; }

Q3:解释 wasm32 与 wasm64 的区别。何时需要使用 wasm64?

Q4:列出 Emscripten 默认禁用 C++ 异常与 RTTI 的原因。

Q5:什么是 WASI?它与传统 Emscripten 输出有何区别?

进阶题

Q6:使用 embind 暴露一个 C++ 类,包含构造、析构、虚函数、智能指针。

Q7:分析以下 Wasm 模块为何体积过大,给出至少 3 条优化建议:

emcc main.cpp -o main.js -g -O0
# main.wasm: 5MB

Q8:使用 pthread 在 Wasm 中实现一个并行计算,要求 4 个线程协同处理一个大数组。

Q9:分析 SharedArrayBuffer 在浏览器中的安全限制,以及为什么需要 COOP/COEP 头。

Q10:对比 embind 与 WebIDL Binder 的优劣,给出选择建议。

挑战题

Q11:设计一个完整的端到端项目,要求:

  • C++ 实现核心算法(如图像卷积);
  • 使用 CMake 与 Emscripten 构建为 ES Module;
  • 提供 TypeScript 类型定义;
  • 集成到 React 应用;
  • 部署到 GitHub Pages。

Q12:使用 wasm-opt 与 Binaryen IR 编写自定义 Wasm 优化 pass,将一段 C++ 编译的 Wasm 体积减小至少 20%。

Q13:使用 WASI 组件模型(component model)让 C++ Wasm 模块与 Rust Wasm 模块互操作。

Q14:实现一个 Wasm 模块,要求使用 SIMD 与 threads 在浏览器中并行计算 Mandelbrot 集合,并实时显示在 Canvas 上。要求达到 60 FPS。

Q15:分析浏览器 Streaming Compilation 与同步 Compilation 的性能差异,设计实验量化启动延迟。