Emscripten:C/C++ 到 WebAssembly
Emscripten 是什么?
Emscripten 是一个完整的 C/C++ 到 WebAssembly 编译工具链,由 Alon Zakai 于 2010 年创建(最初编译到 asm.js)。它不仅包含编译器前端 emcc,还模拟了大量的 POSIX API(文件系统、网络、线程等),让现有的 C/C++ 库几乎可以不修改地编译到 Wasm。
安装 Emscripten SDK
# 安装 emsdk
git clone https://github.com/emscripten-core/emsdk.git
cd emsdk
# 安装并激活最新版本
./emsdk install latest
./emsdk activate latest
# 配置环境变量(每次新终端需要执行)
source ./emsdk_env.sh
# 验证安装
emcc --version # emcc (Emscripten gcc/clang-like replacement) 3.x.x
编译 C 代码
// hello.c
#include <stdio.h>
#include <math.h>
int add(int a, int b) {
return a + b;
}
double sqrt_approx(double x) {
return sqrt(x);
}
// EMSCRIPTEN_KEEPALIVE 防止函数被 DCE(死代码消除)
int process_array(int* arr, int len) {
int sum = 0;
for (int i = 0; i < len; i++) sum += arr[i];
return sum;
}
# 编译为 Wasm(生成 hello.js + hello.wasm)
emcc hello.c -o hello.js -s WASM=1 -s EXPORTED_FUNCTIONS='["_add","_sqrt_approx","_process_array"]' -s EXPORTED_RUNTIME_METHODS='["ccall","cwrap"]' -O3
# 编译为纯 ES 模块(不生成 HTML)
emcc hello.c -o hello.mjs -s WASM=1 -s MODULARIZE=1 -s EXPORT_ES6=1 -O3
ccall 和 cwrap:调用 C 函数
// 在 HTML 中加载并使用
const Module = await import('./hello.mjs');
await Module.ready;
// ccall:直接调用 C 函数
// ccall(函数名, 返回类型, 参数类型数组, 参数值数组)
const result = Module.ccall('add', 'number', ['number', 'number'], [3, 4]);
console.log(result); // 7
// cwrap:创建可重复调用的 JS 函数包装器
const add = Module.cwrap('add', 'number', ['number', 'number']);
console.log(add(10, 20)); // 30
// 处理数组:通过 Emscripten 的堆内存
function processArray(arr) {
const bytes = arr.length * Int32Array.BYTES_PER_ELEMENT;
const ptr = Module._malloc(bytes);
Module.HEAP32.set(arr, ptr / Int32Array.BYTES_PER_ELEMENT);
const sum = Module.ccall('process_array', 'number',
['number', 'number'], [ptr, arr.length]);
Module._free(ptr);
return sum;
}
console.log(processArray([1, 2, 3, 4, 5])); // 15
EM_JS 和 EM_ASM:内联 JavaScript
#include <emscripten.h>
// EM_JS:在 C 中定义一个 JavaScript 函数
EM_JS(void, log_to_console, (const char* str), {
console.log(UTF8ToString(str));
});
// EM_ASM:直接内联 JavaScript 代码片段
void show_progress(int percent) {
EM_ASM({
document.getElementById('progress').style.width = $0 + '%';
}, percent);
}
Emscripten 的架构原理
Emscripten 如何将 C/C++ 带进 Web
Emscripten 不只是一个「C→Wasm 编译器」,它是一个完整的仿真环境:
Embind:C++ 类的高级绑定
相比 ccall/cwrap 的优势
ccall/cwrap 只能调用 C 风格函数(基本类型参数)。对于 C++ 类、模板、枚举等,应使用 Embind 来生成更自然的 JavaScript 绑定。
// matrix.cpp — 使用 Embind 绑定 C++ 类
#include <emscripten/bind.h>
#include <vector>
#include <stdexcept>
class Matrix {
public:
Matrix(int rows, int cols)
: rows_(rows), cols_(cols), data_(rows * cols, 0.0) {}
void set(int row, int col, double val) {
if (row >= rows_ || col >= cols_) throw std::out_of_range("Index out of bounds");
data_[row * cols_ + col] = val;
}
double get(int row, int col) const {
if (row >= rows_ || col >= cols_) throw std::out_of_range("Index out of bounds");
return data_[row * cols_ + col];
}
Matrix multiply(const Matrix& other) const;
int rows() const { return rows_; }
int cols() const { return cols_; }
private:
int rows_, cols_;
std::vector<double> data_;
};
// Embind 绑定声明
EMSCRIPTEN_BINDINGS(matrix_module) {
emscripten::class_<Matrix>("Matrix")
.constructor<int, int>() // 导出构造函数
.function("set", &Matrix::set) // 导出成员函数
.function("get", &Matrix::get)
.function("multiply", &Matrix::multiply)
.property("rows", &Matrix::rows) // 导出只读属性
.property("cols", &Matrix::cols);
}
# 使用 Embind 编译(--bind 标志)
emcc matrix.cpp -o matrix.js \
-s WASM=1 \
--bind \
-O2
// JavaScript 使用 Embind 绑定的类
const Module = await import('./matrix.js');
await Module.ready;
// 像使用普通 JS 类一样!
const a = new Module.Matrix(2, 2); // 调用 C++ 构造函数
a.set(0, 0, 1); a.set(0, 1, 2);
a.set(1, 0, 3); a.set(1, 1, 4);
console.log(a.rows); // 2(属性访问)
console.log(a.get(0, 1)); // 2
// ❗ 重要:C++ 对象需要手动释放!
a.delete(); // 调用 C++ 析构函数,释放内存
JavaScript 的垃圾回收器(GC)只管理 JS 对象的内存,不会自动调用 C++ 析构函数。用 Embind 创建的 C++ 对象(如 new Module.Matrix(2,2))在 JS 侧只是一个轻量包装,内部的 C++ 内存只有在显式调用 .delete() 时才会释放。在函数或循环中大量创建 Embind 对象而不 delete,会造成严重的内存泄漏,最终导致浏览器 OOM(内存不足)崩溃。建议使用 try-finally 或类似 defer 的包装函数确保 delete 被调用。
Emscripten 文件系统操作
// 使用 Emscripten 预加载文件
#include <stdio.h>
#include <emscripten.h>
int process_file(const char* path) {
FILE* f = fopen(path, "rb"); // 在 MEMFS 中打开文件
if (!f) return -1;
char buf[256];
fread(buf, 1, sizeof(buf), f);
fclose(f);
// 处理 buf...
return 0;
}
# 将文件预嵌入 Wasm 模块(打包进 .js 中的 base64)
emcc processor.c -o processor.js \
--preload-file data/model.bin \ # 单个文件
--preload-file assets/ # 整个目录
-s WASM=1
# 编译时嵌入(embed-file,编译进模块,无需 .data 文件)
emcc processor.c -o processor.js \
--embed-file data/model.bin \
-s WASM=1
如果你的 C 代码需要传递数组或字符串(需要 _malloc/_free),必须在 EXPORTED_FUNCTIONS 中显式列出它们:-s EXPORTED_FUNCTIONS='["_malloc","_free","_my_func"]'。否则 Emscripten 的 DCE(死代码消除)会将它们从输出中删除,导致运行时 "not a function" 错误。函数名前需要加 _ 前缀(C 符号的 Emscripten 约定)。
常用编译标志参考
本章小结
- Emscripten 是完整的 C/C++ Web 移植平台:不只编译代码,还仿真了 POSIX 文件系统、网络、线程,让大型 C++ 库(FFmpeg、OpenCV、SQLite)几乎无修改地运行在浏览器
- ccall/cwrap:适合简单 C 函数调用;Embind:适合 C++ 类和复杂对象绑定;EM_JS/EM_ASM:在 C 中内联 JavaScript 代码
- Embind 对象必须手动 delete():JS 垃圾回收不会自动触发 C++ 析构函数,内存泄漏风险真实存在
- 文件系统:--preload-file 在模块加载时预加载文件到 MEMFS;--embed-file 编译进模块,适合小型数据文件
- 体积优化:-s MINIMAL_RUNTIME=1 可大幅减小胶水代码;-O3 开启编译器优化;wasm-opt 再做二次优化