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

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.htmlhello.html, hello.js, hello.wasm直接在浏览器中打开
-o hello.jshello.js, hello.wasm自行集成到项目中
-o hello.wasmhello.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.HEAPF3232 位浮点视图Float32Array
Module.HEAPF6464 位浮点视图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_JSEM_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输出 Wasm1
-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生成模块化 JS0
-s EXPORT_NAME='MyModule'模块名Module
-s FILESYSTEM=0禁用虚拟文件系统1
-s DISABLE_EXCEPTION_CATCHING=1禁用异常捕获0
-s SINGLE_FILE=1将 Wasm 嵌入 JS0

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 模块。