强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

WebAssembly 入门教程 / 05 - C/C++ 编译到 Wasm

05 - C/C++ 编译到 Wasm

将现有 C/C++ 库移植到 Web 上,是 WebAssembly 最成熟的使用场景之一。


5.1 Emscripten 编译基础

最简单的编译

// hello.c
#include <stdio.h>

int main() {
    printf("Hello from C!\n");
    return 0;
}
# 编译为 HTML + JS + Wasm
emcc hello.c -o hello.html

# 编译为纯 JS + Wasm
emcc hello.c -o hello.js

# 编译为纯 Wasm(需要 -s STANDALONE_WASM)
emcc hello.c -o hello.wasm -s STANDALONE_WASM

输出文件对照

命令 输出文件 用途
-o hello.html hello.html, hello.js, hello.wasm 直接在浏览器中打开
-o hello.js hello.js, hello.wasm 自行集成到项目中
-o hello.wasm hello.wasm 独立 Wasm 模块

5.2 导出函数给 JavaScript

使用 EXPORTED_FUNCTIONS

// math_utils.c
#include <emscripten.h>

EMSCRIPTEN_KEEPALIVE
int factorial(int n) {
    if (n <= 1) return 1;
    return n * factorial(n - 1);
}

EMSCRIPTEN_KEEPALIVE
double power(double base, int exp) {
    double result = 1.0;
    for (int i = 0; i < exp; i++) {
        result *= base;
    }
    return result;
}
emcc math_utils.c -o math_utils.js \
  -s EXPORTED_FUNCTIONS='["_factorial","_power","_malloc","_free"]' \
  -s EXPORTED_RUNTIME_METHODS='["ccall","cwrap"]' \
  -O2
<script src="math_utils.js"></script>
<script>
Module.onRuntimeInitialized = () => {
  // 方式 1:使用 ccall(自动处理类型转换)
  const result = Module.ccall('factorial', 'number', ['number'], [10]);
  console.log('10! =', result);  // 3628800

  // 方式 2:使用 cwrap(创建可复用的函数引用)
  const power = Module.cwrap('power', 'number', ['number', 'number']);
  console.log('2^10 =', power(2, 10));  // 1024
};
</script>

ccall / cwrap 参数说明

Module.ccall(funcName, returnType, argTypes, args)

returnType / argTypes 可选值:
  'number'  — 数值类型 (i32, i64, f32, f64)
  'string'  — 字符串(自动处理 UTF-8 编码/解码)
  'boolean' — 布尔值
  'array'   — 数组(自动传递指针)
  'void'    — 无返回值
  null      — 返回 void

5.3 内存管理

C 侧的堆内存

// array_ops.c
#include <stdlib.h>
#include <string.h>
#include <emscripten.h>

EMSCRIPTEN_KEEPALIVE
int* create_array(int size) {
    int* arr = (int*)malloc(size * sizeof(int));
    for (int i = 0; i < size; i++) {
        arr[i] = i * i;
    }
    return arr;
}

EMSCRIPTEN_KEEPALIVE
int sum_array(int* arr, int size) {
    int sum = 0;
    for (int i = 0; i < size; i++) {
        sum += arr[i];
    }
    return sum;
}

EMSCRIPTEN_KEEPALIVE
void free_array(int* arr) {
    free(arr);
}
// JavaScript 侧操作 Wasm 内存
Module.onRuntimeInitialized = () => {
  const N = 100;
  
  // 在 Wasm 堆上分配数组
  const ptr = Module._create_array(N);
  
  // 从 Wasm 内存中读取数据
  const heap = new Int32Array(Module.HEAP32.buffer, ptr, N);
  console.log('前 10 个元素:', Array.from(heap.slice(0, 10)));
  
  // 计算总和
  const sum = Module._sum_array(ptr, N);
  console.log('总和:', sum);
  
  // 释放内存
  Module._free_array(ptr);
};

内存视图

视图类型 说明 对应 JS 类型
Module.HEAP8 有符号 8 位视图 Int8Array
Module.HEAPU8 无符号 8 位视图 Uint8Array
Module.HEAP16 有符号 16 位视图 Int16Array
Module.HEAPU16 无符号 16 位视图 Uint16Array
Module.HEAP32 有符号 32 位视图 Int32Array
Module.HEAPU32 无符号 32 位视图 Uint32Array
Module.HEAPF32 32 位浮点视图 Float32Array
Module.HEAPF64 64 位浮点视图 Float64Array

字符串传递

// string_utils.c
#include <emscripten.h>
#include <string.h>
#include <stdlib.h>
#include <ctype.h>

EMSCRIPTEN_KEEPALIVE
char* to_upper(const char* input) {
    int len = strlen(input);
    char* result = (char*)malloc(len + 1);
    for (int i = 0; i < len; i++) {
        result[i] = toupper(input[i]);
    }
    result[len] = '\0';
    return result;
}

// 返回新字符串长度
EMSCRIPTEN_KEEPALIVE
int get_string_length(const char* str) {
    return strlen(str);
}
emcc string_utils.c -o string_utils.js \
  -s EXPORTED_FUNCTIONS='["_to_upper","_get_string_length","_malloc","_free"]' \
  -s EXPORTED_RUNTIME_METHODS='["ccall","cwrap","UTF8ToString","stringToUTF8","lengthBytesUTF8"]'
Module.onRuntimeInitialized = () => {
  // ccall 传字符串自动处理
  const result = Module.ccall('to_upper', 'string', ['string'], ['hello world']);
  console.log(result);  // "HELLO WORLD"
};

5.4 EM_JS 宏

EM_JS 允许在 C 代码中直接内联 JavaScript 代码:

#include <emscripten.h>

// EM_JS: 在 C 中定义一个 JS 实现的函数
EM_JS(void, js_console_log, (const char* str), {
  // 这里的代码是 JavaScript
  console.log('From C:', UTF8ToString(str));
});

EM_JS(int, js_get_timestamp, (), {
  return Date.now();
});

// EM_ASYNC_JS: 异步 JS 函数
EM_ASYNC_JS(int, js_fetch_data, (const char* url), {
  const response = await fetch(UTF8ToString(url));
  const text = await response.text();
  console.log('Fetched:', text.length, 'bytes');
  return text.length;
});

int main() {
    js_console_log("Hello from C via EM_JS!");
    int ts = js_get_timestamp();
    js_console_log("Timestamp recorded");
    return 0;
}

EM_JS vs EM_ASM

// EM_ASM: 简单内联(不支持返回值和参数类型声明)
EM_ASM({
  console.log('Hello from EM_ASM');
  console.log('Arg0:', $0);
  console.log('Arg1:', $1);
}, 42, 3.14);

// EM_ASM_INT: 返回 i32
int result = EM_ASM_INT({
  return $0 + $1;
}, 10, 20);

// EM_ASM_DOUBLE: 返回 f64
double val = EM_ASM_DOUBLE({
  return Math.PI * $0;
}, 2.0);

EM_JS vs EM_ASM 对比

特性 EM_JS EM_ASM
可读性 ✅ 更好 ⚠️ 一般
参数类型声明 ✅ 有 ❌ 无
返回值类型 ✅ 声明式 ⚠️ 需要 _INT/_DOUBLE 变体
复用性 ✅ 可在多处调用 ❌ 内联一次
推荐场景 复杂 JS 交互 简单的临时调用

5.5 与 JS 的交互模式

JS 调用 C 函数(同步)

// add.c
#include <emscripten.h>

EMSCRIPTEN_KEEPALIVE
int add(int a, int b) {
    return a + b;
}

C 调用 JS 函数(通过导入)

// 使用 extern 声明外部 JS 函数
extern int js_random(void);
extern void js_report(int result);

EMSCRIPTEN_KEEPALIVE
void process_data(int n) {
    int total = 0;
    for (int i = 0; i < n; i++) {
        total += js_random();
    }
    js_report(total);
}
const imports = {
  env: {
    js_random: () => Math.floor(Math.random() * 100),
    js_report: (result) => console.log('Result:', result)
  }
};

// 编译时需要声明这些导入

共享内存协作

C 端:                               JS 端:
┌────────────────┐                  ┌────────────────┐
│  计算引擎       │                  │  UI / 网络      │
│  (Wasm)        │                  │  (JavaScript)   │
│                │    共享内存       │                │
│  写入结果到    │◄═══════════════►│  读取结果并     │
│  线性内存      │   (Memory)       │  渲染到 Canvas  │
└────────────────┘                  └────────────────┘

5.6 CMake 与大型项目

CMakeLists.txt 配置

cmake_minimum_required(VERSION 3.13)
project(my_wasm_project)

# 设置 Emscripten 编译选项
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -O2")
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} \
  -s WASM=1 \
  -s EXPORTED_FUNCTIONS='[_process_image,_malloc,_free]' \
  -s EXPORTED_RUNTIME_METHODS='[ccall,cwrap]' \
  -s ALLOW_MEMORY_GROWTH=1 \
  -s MODULARIZE=1 \
  -s EXPORT_NAME='MyModule'")

add_executable(my_module src/main.c src/image_process.c)

# 链接第三方库
# find_package(PNG REQUIRED)
# target_link_libraries(my_module PNG::PNG)
# 使用 Emscripten 的 CMake 工具链文件
mkdir build && cd build
emcmake cmake ..
emmake make

移植现有 C 库

# 示例:移植 zlib
git clone https://github.com/nicedoc/zlib.git
cd zlib
emcmake cmake . -DCMAKE_INSTALL_PREFIX=$HOME/wasm-libs
emmake make
emmake make install

# 编译自己的代码时链接 zlib
emcc my_code.c -o my_code.js \
  -I$HOME/wasm-libs/include \
  -L$HOME/wasm-libs/lib -lz

5.7 优化选项

优化级别

选项 说明 适用场景
-O0 无优化 开发调试
-O1 基本优化 快速构建
-O2 标准优化 生产环境
-O3 激进优化 追求性能
-Os 体积优化 追求小体积
-Oz 最大体积优化 极致压缩

体积优化技巧

# 1. 使用 -Oz 优化
emcc input.c -o output.js -Oz

# 2. 使用 wasm-opt 进一步优化
wasm-opt -Oz output.wasm -o output_optimized.wasm

# 3. 只导出需要的函数
emcc input.c -o output.js \
  -s EXPORTED_FUNCTIONS='["_needed_func"]' \
  -s EXPORTED_RUNTIME_METHODS='[]'

# 4. 剥离调试信息
emcc input.c -o output.js -O2 --strip-debug

# 5. 禁用不需要的功能
emcc input.c -o output.js \
  -s DISABLE_EXCEPTION_CATCHING=1 \
  -s FILESYSTEM=0  # 禁用虚拟文件系统

链接选项参考

选项 说明 默认值
-s WASM=1 输出 Wasm 1
-s ALLOW_MEMORY_GROWTH=1 允许内存增长 0
-s INITIAL_MEMORY=16777216 初始内存(16MB) 16MB
-s MAXIMUM_MEMORY=268435456 最大内存(256MB) 2GB
-s STACK_SIZE=65536 栈大小(64KB) 64KB
-s MODULARIZE=1 生成模块化 JS 0
-s EXPORT_NAME='MyModule' 模块名 Module
-s FILESYSTEM=0 禁用虚拟文件系统 1
-s DISABLE_EXCEPTION_CATCHING=1 禁用异常捕获 0
-s SINGLE_FILE=1 将 Wasm 嵌入 JS 0

5.8 多文件项目示例

project/
├── CMakeLists.txt
├── src/
│   ├── main.c
│   ├── math/
│   │   ├── vector.h
│   │   └── vector.c
│   └── image/
│       ├── filter.h
│       └── filter.c
├── include/
│   └── common.h
└── build/
    ├── project.js
    └── project.wasm
// include/common.h
#ifndef COMMON_H
#define COMMON_H

#include <emscripten.h>

#define EXPORT EMSCRIPTEN_KEEPALIVE

#endif

// src/math/vector.h
#ifndef VECTOR_H
#define VECTOR_H

typedef struct {
    float x, y, z;
} Vec3;

Vec3 vec3_add(Vec3 a, Vec3 b);
float vec3_length(Vec3 v);
Vec3 vec3_normalize(Vec3 v);

#endif

// src/math/vector.c
#include "vector.h"
#include <math.h>
#include "common.h"

EXPORT
Vec3 vec3_add(Vec3 a, Vec3 b) {
    return (Vec3){a.x + b.x, a.y + b.y, a.z + b.z};
}

EXPORT
float vec3_length(Vec3 v) {
    return sqrtf(v.x * v.x + v.y * v.y + v.z * v.z);
}

EXPORT
Vec3 vec3_normalize(Vec3 v) {
    float len = vec3_length(v);
    return (Vec3){v.x / len, v.y / len, v.z / len};
}

5.9 调试 C/C++ → Wasm

生成调试信息

# 使用 -g 参数保留调试信息
emcc debug.c -o debug.js -g -s ASSERTIONS=1

# 生成 source map
emcc debug.c -o debug.js -g4 --source-map-base http://localhost:8080/

Chrome DevTools 调试

1. 打开 Chrome DevTools
2. Sources → 左侧会出现 Wasm 的 C 源码
3. 可以直接在源码中设置断点
4. 支持查看变量值、调用栈

常见错误排查

错误 原因 解决方案
RuntimeError: memory access out of bounds 缓冲区溢出 检查数组边界
RuntimeError: unreachable 除零、abort() 调用 检查除法、断言
LinkError: WebAssembly.instantiate 导入不匹配 检查导入对象
Cannot call ... due to unbound types 类型不匹配 检查 ccall 参数类型

5.10 注意事项

⚠️ 不支持的功能:Emscripten 不支持 fork()pthread_create()(需 SharedArrayBuffer + 特殊编译选项)、部分系统调用。移植前需评估可行性。

⚠️ 异常处理:C++ 异常默认使用 Emscripten 的 JavaScript 异常实现,性能较差。建议使用 -fno-exceptions-s DISABLE_EXCEPTION_CATCHING=1 禁用。

⚠️ 虚拟文件系统:Emscripten 提供了虚拟文件系统(FS),但性能不如原生文件系统。尽量避免大量文件 I/O 操作。

⚠️ 64 位整数:i64 在 JS 中无法直接表示为 Number,需要使用 BigInt 或拆分为两个 i32。


5.11 扩展阅读


下一章06 - Rust 编译到 Wasm — 使用 Rust 和 wasm-pack 构建高质量的 Wasm 模块。