Emscripten 教程:从 C/C++ 到 WebAssembly 的完整指南
目录
- 什么是 Emscripten?
- 为什么使用 Emscripten?
- 环境准备
- 安装 Emscripten SDK
- 验证安装
- 你的第一个 "Hello, World!" 程序
- 编译 C 代码
- 运行生成的 Web 应用
- 深入理解编译过程
- 生成的文件 (
*.js,*.wasm,*.html) emcc常用选项
- 生成的文件 (
- 与 JavaScript 交互
- 从 C/C++ 调用 JavaScript
- 从 JavaScript 调用 C/C++ 函数
- 处理 DOM 和浏览器 API
- 使用
emscripten.h提供的辅助函数
- 使用
- 进阶主题
- 使用 C++ 和 STL
- 文件系统 (
--embed-file或--preload-file)
- 性能优化
- 指定编译目标 (
-O1,-O2,-O3) - 启用 SIMD
- 内存模型 (
ALLOW_MEMORY_GROWTH)
- 指定编译目标 (
- 总结与资源
什么是 Emscripten?
Emscripten 是一个 LLVM 到 JavaScript 的编译器,它将用 C/C++ (以及其他可以被 LLVM 前端处理的语言) 编写的代码编译成 WebAssembly 和 JavaScript。

你可以把它想象成一个特殊的“翻译官”,它能听懂 C/C++ 的语言,并将其翻译成浏览器能够理解和运行的 WebAssembly 代码,为了在浏览器中启动和运行 WebAssembly,它还会生成一个“胶水” JavaScript 文件。
核心组件:
emcc: 命令行工具,是使用 Emscripten 的主要入口。- LLVM: Emscripten 依赖 LLVM 来分析和优化 C/C++ 代码。
- Clang: Emscripten 使用 Clang 作为其 C/C++ 前端,将代码解析成 LLVM 的中间表示。
- Binaryen: 一个用于 WebAssembly 的优化器后端。
为什么使用 Emscripten?
将现有的 C/C++ 代码库(如游戏引擎、科学计算库、图像处理工具)带到 Web 上,而无需用 JavaScript 重写。
- 性能: WebAssembly 提供了接近原生的性能,非常适合计算密集型任务,如游戏、物理模拟、视频/音频处理等。
- 代码复用: 你可以为 Web、桌面、移动设备等多个平台使用同一套核心 C++ 代码库。
- 利用现有生态: 可以轻松地将像 OpenCV、SQLite、TensorFlow 等成熟的 C++ 库带到浏览器中。
- 类型安全与工具链: C++ 的静态类型系统和成熟的开发工具链可以带来更稳定、更易于维护的大型项目。
环境准备
Emscripten 依赖于 Python 3, Node.js, 和一些系统工具,最推荐的方式是使用官方提供的 SDK,它会自动处理所有依赖。

安装 Emscripten SDK
-
确保你有以下工具:
- Python 3.6+
- Node.js (推荐 v14+)
- 一个现代的 C++ 编译器 (如 GCC/Clang 或 MSVC),虽然 Emscripten 主要使用自己的工具,但有时系统编译器用于构建某些依赖。
-
下载并设置 SDK: 打开你的终端 (Windows 上是 PowerShell 或 Git Bash),然后运行以下命令:
# 克隆 Emscripten SDK 仓库 git clone https://github.com/emscripten-core/emsdk.git # 进入 emsdk 目录 cd emsdk # 下载最新的工具 (可能需要几分钟) ./emsdk install latest # 激活最新的工具 ./emsdk activate latest # 设置环境变量 (非常重要!) # 在 Linux/macOS 上: source ./emsdk_env.sh # 在 Windows (PowerShell) 上: .\emsdk_env.ps1
验证安装
在同一个终端窗口中,运行以下命令来检查 emcc 是否可用:
emcc -v
如果看到版本信息和一堆编译器输出,说明安装成功了!

重要提示: 每次打开一个新的终端窗口时,都需要先进入
emsdk目录并运行source ./emsdk_env.sh(或 Windows 上的等效命令) 来设置环境变量,为了避免麻烦,你可以将这个命令添加到你 shell 的配置文件中 (如~/.bashrc,~/.zshrc, 或 Windows 的环境变量)。
你的第一个 "Hello, World!" 程序
让我们从最简单的例子开始。
创建 C 源文件
创建一个名为 hello.c 的文件,内容如下:
// hello.c
#include <stdio.h>
int main() {
printf("Hello, World from C!\n");
return 0;
}
编译 C 代码
在终端中,使用 emcc 命令来编译它:
emcc hello.c -o hello.html
这会生成三个文件:
hello.html: 一个简单的 HTML 文件,用于加载和运行你的程序。hello.js: 胶水 JavaScript 文件,负责加载 WebAssembly 模块并与浏览器交互。hello.wasm: 你的 C 代码编译成的 WebAssembly 模块。
运行生成的 Web 应用
Emscripten 自带了一个简单的本地服务器,可以避免浏览器的 CORS (跨域资源共享) 限制。
在终端中运行:
emrun hello.html
它会提示你打开一个浏览器地址 (通常是 http://localhost:8080),在浏览器中访问这个地址,你将在开发者工具的控制台中看到输出:
Hello, World from C!
深入理解编译过程
生成的文件
hello.wasm: 核心,包含了你 C 代码编译后的机器码,浏览器可以直接执行。hello.js: 运行时库和胶水代码,它负责:- 从服务器下载
hello.wasm文件。 - 编译和实例化 WebAssembly 模块。
- 提供一个
Module对象,让 JavaScript 可以与 Wasm 交互。 - 处理像
printf这样的函数,将 C 的标准输出重定向到浏览器的控制台。
- 从服务器下载
hello.html: 一个基础的 HTML 模板,它会自动加载并运行hello.js。
emcc 常用选项
-o <output_file>: 指定输出文件名,Emscripten 会根据扩展名决定生成什么。hello.js: 只生成 JS 文件 (用于 Node.js 或作为库)。hello.html: 生成一个可立即在浏览器中运行的完整 Web 应用。hello.wasm: 只生成 Wasm 文件 (不常用,因为无法独立运行)。
-s <OPTION_NAME>=<value>: 设置 Emscripten 的特殊标志,这是最强大的选项之一。-s ALLOW_MEMORY_GROWTH=1允许 Wasm 模块在运行时动态增加内存。
-O1,--O2,-O3: 优化级别,类似 GCC/Clang。-O3性能最高,但编译时间最长,生成的文件也最大,对于发布版本,推荐使用-O2或-O3。--bind: 在编译 C++ 代码时使用,它会自动生成“胶水”代码,使 C++ 类和函数更容易被 JavaScript 调用。
与 JavaScript 交互
这是 Emscripten 的核心功能之一。
从 C/C++ 调用 JavaScript
使用 EM_ASM 宏,它允许你在 C 代码中直接嵌入 JavaScript 代码。
修改 hello.c:
// hello_interactive.c
#include <stdio.h>
#include <emscripten.h> // 必须包含此头文件
int main() {
printf("C: Hello, World!\n");
// 调用一段简单的 JavaScript 代码
EM_ASM({
console.log("JS: Hello from inside EM_ASM!");
});
return 0;
}
编译并运行:
emcc hello_interactive.c -o hello_interactive.html --bind emrun hello_interactive.html
输出:
C: Hello, World!
JS: Hello from inside EM_ASM!
EM_ASM 可以接收 C/C++ 的变量作为参数:
int number = 42;
const char* text = "from C";
EM_ASM({
console.log("The number is:", $0, "and the text is:", $1);
}, number, text);
$0, $1 等占位符会被 C/C++ 的变量替换。
从 JavaScript 调用 C/C++ 函数
这个过程稍微复杂一些,需要两步:
- 在 C/C++ 中使用
EMSCRIPTEN_KEEPALIVE标记要导出的函数。 - 在 JavaScript 中通过
Module.cwrap或Module.asm来获取并调用该函数。
C 代码 (function.c):
#include <emscripten.h>
// 这个函数将被 JavaScript 调用
int add(int a, int b) {
return a + b;
}
// EMSCRIPTEN_KEEPALIVE 告诉编译器不要优化掉这个函数
EMSCRIPTEN_KEEPALIVE
int multiply(int a, int b) {
return a * b;
}
HTML (function.html):
<!DOCTYPE html>
<html>
<head>JS to C Call</title>
</head>
<body>
<h1>Check the console</h1>
<script src="function.js"></script>
<script>
// Module.cwrap 用于导出 C 函数,使其像普通的 JS 函数一样被调用
// 参数: C函数名, 返回值类型, 参数类型数组
const addFunction = Module.cwrap('add', 'number', ['number', 'number']);
const multiplyFunction = Module.cwrap('multiply', 'number', ['number', 'number']);
// 现在可以像调用普通 JS 函数一样调用它们
const sum = addFunction(5, 7);
const product = multiplyFunction(5, 7);
console.log(`5 + 7 = ${sum}`);
console.log(`5 * 7 = ${product}`);
</script>
</body>
</html>
编译:
emcc function.c -o function.html --bind
运行后,在浏览器控制台可以看到:
5 + 7 = 12
5 * 7 = 35
处理 DOM 和浏览器 API
直接在 C 中调用 document.getElementById 是不可能的,你需要通过 EM_ASM 或 emscripten.h 提供的辅助函数。
使用 emscripten.h 中的函数是更现代和推荐的方式:
#include <stdio.h>
#include <emscripten.h>
#include <emscripten/html5.h>
void printMessage() {
printf("C: About to update the DOM.\n");
// EM_ASM 可以直接操作 DOM
EM_ASM({
document.body.innerHTML += '<p>JS: This paragraph was added from C!</p>';
});
}
int main() {
printMessage();
return 0;
}
进阶主题
使用 C++ 和 STL
Emscripten 完全支持 C++ 和 STL,你只需要编译 .cpp 文件即可。
vector_example.cpp:
#include <vector>
#include <iostream>
#include <emscripten.h>
extern "C" { // 使用 C 链接约定,方便 cwrap 调用
EMSCRIPTEN_KEEPALIVE
int sumVector(const int* array, int length) {
std::vector<int> v(array, array + length);
int sum = 0;
for (int num : v) {
sum += num;
}
return sum;
}
}
vector.html:
<script src="vector.js"></script>
<script>
const array = new Int32Array([1, 2, 3, 4, 5]);
const sumFunction = Module.cwrap('sumVector', 'number', ['array', 'number']);
const result = sumFunction(array, array.length);
console.log(`The sum is: ${result}`); // 输出: The sum is: 15
</script>
编译:
emcc vector_example.cpp -o vector.html -s ALLOW_MEMORY_GROWTH=1 -O3
注意: 使用 STL 时,Wasm 文件会变大,因为需要包含 STL 的库代码。
-s ALLOW_MEMORY_GROWTH=1对于处理动态大小的 STL 容器(如std::vector)非常有用。
文件系统
你可以将 C 程序需要的文件(如图片、文本、数据)打包进最终的 Wasm 文件中。
file.c:
#include <stdio.h>
#include <emscripten.h>
int main() {
FILE *file = fopen("my_data.txt", "r");
if (file) {
char buffer[256];
while (fgets(buffer, sizeof(buffer), file)) {
printf("C read from file: %s", buffer);
}
fclose(file);
} else {
printf("C could not open my_data.txt\n");
}
return 0;
}
编译时嵌入文件:
emcc file.c -o file.html --embed-file my_data.txt
这会生成 file.html, file.js, file.wasm,file.js 会自动处理 my_data.txt 的加载,使其在 Wasm 文件内部可被 fopen 访问。
性能优化
- 优化级别: 使用
-O2或-O3进行发布编译。 - SIMD: 对于计算密集型任务,启用 SIMD (单指令多数据流) 可以显著提升性能。
emcc ... -s USE_SAFESIMD=1
- 内存增长: 如果你的应用需要动态分配大量内存(加载大文件或使用 STL),启用
ALLOW_MEMORY_GROWTH。emcc ... -s ALLOW_MEMORY_GROWTH=1
- 剥离调试信息: 发布时,剥离调试信息可以减小文件大小。
emcc ... -g0
总结与资源
Emscripten 是一个功能极其强大的工具,它为 Web 开发者打开了利用整个 C/C++ 生态的大门,虽然初看起来可能有些复杂,但掌握了基本流程(编译、交互、优化)后,你就能将几乎任何高性能的桌面应用带到浏览器中。
官方资源
- Emscripten 官方文档: https://emscripten.org/docs/ (这是最重要的资源,最权威、最全面)
- Emscripten 教程: https://emscripten.org/docs/getting_started/downloads.html (官方的入门教程)
- Emscripten 主页: https://emscripten.org/
希望这份教程能帮助你顺利入门 Emscripten!祝你编码愉快!
