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 扩展阅读
- Emscripten 官方文档
- Emscripten API 参考
- Emscripten 移植指南
- WebAssembly C/C++ 入门
- Figma 技术博客 — How Figma built the WebGL plugin system
下一章:06 - Rust 编译到 Wasm — 使用 Rust 和 wasm-pack 构建高质量的 Wasm 模块。